ThreadLocal源码夺命12问,你能坚持到第几问?

简介: 临近秋招,备战暑期实习,祝大家每天进步亿点点!Day10

本篇总结的是 并发编程 ThreadLocal源码面试题,后续会每日更新~

ThreadLocal 源码深入分析参考我的往期博客:ThreadLocal源码分析_01 入门案例以及表层源码分析、ThreadLocal源码分析_02 内核(ThreadLocalMap)

注:不理解 ThreadLocal 如何使用以及其原理的小伙伴,一定要先看上面的两篇博客哦


image.png

image.png

、面试官:请你说一说你对ThreadLocal的理解?

ThreadLocal 是一个全局对象,ThreadLocal 是线程范围内变量共享的解决方案;ThreadLocal 可以看作是一个map集合,key就是当前线程,value就是要存放的变量。

参考回答:


ThreadLocal 对象可以给每个线程分配一份属于自己的局部变量副本,多个线程之间可以互不干扰。一般我们会重写 initalValue()方法来给当前 ThreadLocal 对象赋初始值。


2、面试官:简单描述一下JDK1.8中,ThreadLocal原理?

参考回答:


JDK8 中,每个线程对象 Thread 类内部都有一个成员属性 threadLocals(即ThreadLocalMap,它是一个Entry[]数组,而不是 Map 集合哦~),各个线程在调用同一个 ThreadLocal 对象的set(value)方法设置值的时候,就是往各自的 ThreadLocalMap 对象数组中新增值。

ThreadLocalMap (Entry[]数组)中存放的是一个个的 Entry节点,它有两个属性字段,弱引用 key(ThreadLocal对象) ,和强引用 value (当前线程变量副本的值)。

3、面试官:ThreadLocal是怎样坐到线程互不干扰的呢(线程隔离)?

参考回答:


首先,每个线程 Thread 都有一份属于自己的 ThreadLoacalMap 用于存储数据。

当线程访问某个 ThreadLocal 对象的 get()方法时,方法内部会检测该线程的 ThreadLoacalMap 数组(Entry[])内是否存在 key 为当前 ThreadLocal 对象的 Entry 节点。如果数组内没有对应的节点,那么当前 ThreadLocal 对象就会调用其内部的 initialValue() 方法创建一个 Entry 节点存放到 ThreadLocalMap 中去。

4、面试官:ThreadLocal 使用的 hash 是怎样计算得来的?

首先,ThreadLocal 使用的 hash 并不是重写自 Object 的 hashCode() 方法,而是通过自身的nextHashCode();计算得来。代码如下:

// threadLocalHashCode ---> 用于threadLocals的桶位寻址:
// 1.线程获取threadLocal.get()时:
//    如果是第一次在某个threadLocal对象上get,那么就会给当前线程分配一个value,
//    这个value 和 当前的threadLocal对象被包装成为一个 entry 
//    其中entry的 key 是threadLocal对象,value 是threadLocal对象给当前线程生成的value
// 2.这个entry存放到当前线程 threadLocals 这个map的哪个桶位呢? 
//    桶位寻址与当前 threadLocal对象的 threadLocalHashCode有关系:
//    使用 threadLocalHashCode & (table.length - 1) 计算结果得到的位置就是当前 entry 需要存放的位置。
private final int threadLocalHashCode = nextHashCode();
// nextHashCode: 表示hash值
// 创建ThreadLocal对象时会使用到该属性:
// 每创建一个threadLocal对象时,就会使用 nextHashCode 分配一个hash值给这个对象。
private static AtomicInteger nextHashCode = new AtomicInteger();
// HASH_INCREMENT: 表示hash值的增量~
// 每创建一个ThreadLocal对象,ThreadLocal.nextHashCode的值就会增长HASH_INCREMENT(0x61c88647)。
// 这个值很特殊,它是斐波那契数也叫黄金分割数。
// hash增量为这个数字,带来的好处就是hash分布非常均匀。
private static final int HASH_INCREMENT = 0x61c88647;
/**
 * 返回一个nextHashCode的hash值:
 * 创建新的ThreadLocal对象时,使用这个方法,会给当前对象分配一个hash值。
 */
private static int nextHashCode() {
    // 每创建一个对象,nextHashCode计算得到的hash值就增长HASH_INCREMENT(0x61c88647)
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/*
 * 初始化一个起始value:
 * 默认返回null,一般情况下都是需要重写这个方法的(例如第2小节的入门案例中就重写了该方法)。
 */
protected T initialValue() {
    return null;
}

5、面试官:为什么 ThreadLocalMap 选择去重新设计"Map",而不直接使用 JDK中的 HashMap呢?

参考回答:


因为 ThreadLocal 自己重新设计的 Map,它可以把自己的 Key 限定为特有类型(ThreadLocal),这个特定类型的Key 使用的是弱引用 WeakReference>,而 HashMap 中的 Key 采用的是强引用方式。

6、面试官:ThreadLocalMap的Enrty的key为什么要设置成弱引用?

ThreadLocalMap 存储的格式是 Entry。如果使用强引用,当 key 原来对象失效的时候,jvm不会回收 map 里面的 ThreadLocal。

弱引用 WeakReference定义:如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。

ThreadLocalMap 使用 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal 没有外部强引用引用他,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。

站在 ThreadLocalMap 角度就可以区分出哪些 Entry 是过期的,哪些 Entry 是非过期的。

例如:在set()方法向下寻找可用 solt 桶位的过程中,如果碰到key == null 的情况,说明当前Entry 是过期数据,这个时候可以强行占用该桶位,通过replaceStaleEntry方法执行替换过期数据的逻辑。

例如:cleanSomeSlots(int i, int n)方法通过遍历桶位,也会将 key == null 过期数据清理掉。

7、面试官:ThreadLocalMap 对象是何时第一次被创建呢?

参考回答:


每个线程 Thread 对象的 ThreadLocalMap 都是延迟初始化的,当我们在调用 ThreadLocal 对象的 set() 或 get()方法时,它会检测当前线程是否已经绑定了 ThreadLocalMap,如果已经绑定,则继续执行 set() 或 get()方法的逻辑。

而如果没有,则会先创建 ThreadLocalMap 并将其绑定给 Thread 对象。

面试官:那么线程的 ThreadLocalMap 会被多次创建吗?


不会,在线程的生命周期内,ThreadLocalMap 对象只会被初始化一次。

8、面试官:ThreadLocalMap 的初始化长度是多少呢?

初始化时,ThreadLocalMap 容量为 16。

9、面试官:上面你说初始化长度是16,那为什么初始容量要是2的N次幂数呢?

参考回答:


这个设计它和 HashMap 是一样的,目的都是为了方便 hash 寻址时,得到的 index (桶位)更均匀分布,减少 hash 冲突。

寻址算法为:index = threadLocalHashCode & (table.length - 1)。这个算法实际就是取模运算:hash % tab.length,而计算机中直接求余运算效率不如位移运算。

所以源码中做了优化,使用 hash & (tab.length- 1)来寻找桶位。而实际上 hash % length 等于 hash & ( length - 1) 的前提是 length 必须为 2 的 n 次幂。

例如,数组长度 tab.length = 8 的时候,3 & (8 - 1) = 3,2 & (8 - 1) = 2,桶的位置是(数组索引) 3 和 2,不同位置上,不发生 hash 碰撞。


10、面试官:ThreadLocalMap 的 扩容阈值是多少?它的扩容机制是怎样的?

参考回答:


首先,ThreadLocalMap 的扩容阈值为初始容量的 2/3,当数组中,存储 Entry 节点的个数大于等于 2/3 时,会它并不会直接开始扩容。

而是先调用 rehash()方法,在该方法中,全面扫描整个数组,并将数组中过期的数据(key == null)给清理掉,重新整理数组。

如果重新整理数组,并将过期的数据清理后,再次重新判断数组内的 Entry 节点的个数是否达到扩容阈值的3/4,如果达到再调用真正扩容的方法resize();

面试官:那么你对 resize() 方法内部的扩容算法了解吗?


resize() 方法在真正执行扩容时,内部逻辑是先创建一个新的数组,新数组长度是原来数组长度的 2 倍。

然后遍历旧数组,将旧数组中的数据重新按照 hash 算法迁移到新数组里面。

接着重新计算出下次扩容的阈值threshold。

最后更新 Thread 对象的 threadLocals 字段引用,使其指向新数组。

11、面试官:请你说一下 ThreadLocal 的 get 方法的执行流程?

① 首先 get() 方法中会先获取当前线程对象 t : Thread t = Thread.currentThread();

② 接下来根据 t 获取其独有的 ThreadLocalMap 数组:ThreadLocalMap map = getMap(t);

③ 如果 ② 获取的 map为空,则调用setInitialValue()方法,该方法内部调用 initialValue();方法获取 value,并根据 当前线程t 和 value 调用 createMap(t, value); 方法创建 ThradLocalMap。

④ 如果 ② 获取的 map不为空,则直接调用 ThreadLocalMap.Entry e = map.getEntry(this); 方法通过 this(当前ThreadLocal对象)从 ThreadLocalMap 中获取对应封装数据的 Entry 节点。

⑤ 最终通过 T result = (T)e.value; 得到要获取的线程变量副本的值。

注意:


第 ④ 步中,通过当前 ThreadLocal 对象从 ThreadLocalMap 中获取对应封装数据的 Entry 节点时,内部逻辑是需要涉及到桶位寻址 index = threadLocalHashCode & (table.length - 1),如果获取的 inde 桶位中没有目标数据,这时候会执行``nextIndex(int i, int len)方法,**线性的向前或者向后去寻找目标数据所在的桶位,直到遍历整个数组仍未找到,则返回null`**。


此外,在线性的向前、向后遍历数组寻找目标元素所在的桶位时,如果发现数据过期了(key == null),则需要调用expungeStaleEntry(i);方法进行一次探测式过期数据回收。


12、面试官:请你说一下 ThreadLocal 的 set 方法的执行流程?

参考回答:


① 首先,set()方法向 ThreadLocalMap 中添加数据时,也是需要根据 Key (ThreadLocal对象) 的去寻址找到要插入的桶位下标 i = key.threadLocalHashCode & (len-1);

② 根据桶位下标,获取对应桶中的Enety 对象Entry e = tab[i];,如果获取的 e 为 null ,则说明是空桶,直接讲 Key 和 Value 包装成 Entry 放入桶中即可:tab[i] = new Entry(key, value);

③ 如果第 ② 步骤获取的 e 不为 null,说明不是空桶,则需要从以下三种情况考虑:

如果当前桶中 Entry 的 Key 不是当前 ThreadLocal 对象,且不为 null,则调用nextIndex(int i, int len)方法线性查找下一个空桶位,并将新数据放入。

如果当前桶中 Entry 的 Key 是当前 ThreadLocal 对象,则通过更新操作,将就 Entry 的 Value 值覆盖。

如果如果当前桶中 Entry 的 Key 是null,则说明当前 Entry 已经过期,需要执行 替换过期数据的逻辑: replaceStaleEntry(key, value, i);。

总结的面试题也挺费时间的,文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,还请三连支持一下,后续会亿点点的更新!

image.png

相关文章
|
4月前
|
SQL 存储 关系型数据库
MySQL 生产级备份与恢复全攻略:全量 / 增量 / 逻辑 / 物理备份深度拆解 + 误删数据秒级恢复实战
本文系统讲解MySQL备份与恢复体系,涵盖全量/增量、逻辑/物理备份的底层原理与核心差异;详解mysqldump、mydumper、XtraBackup等工具的生产级实战;提供误删数据的多场景快速恢复方案(闪回、延迟从库、回收站);并附Java备份管理模块完整实现。
633 2
|
9月前
|
数据采集 存储 安全
一文讲清数据要素,数据资产,数据治理和数字资产
本文系统梳理了数据要素、数据资产、数据治理与数字资产四大核心概念。数据要素确立数据作为基础生产要素的战略地位;数据资产是企业可控制并带来经济利益的数据资源;数据治理是保障数据质量与安全的管理框架;数字资产则是涵盖数据资产在内的所有数字化有价值资产的统称。厘清四者关系,构建清晰数据认知体系,助力企业高效决策与价值实现。
|
人工智能 自然语言处理 开发工具
《解锁鸿蒙系统AI与第三方应用集成的无限可能》
鸿蒙系统与人工智能技术的融合为应用开发带来新机遇。开发者可利用鸿蒙内置的AI服务(如语音助手、视觉识别等),借助DevEcoStudio等智能工具,快速集成AI功能,降低开发成本。遵循鸿蒙接口规范,确保兼容性和稳定性。参与鸿蒙生态社区,提升开发能力并优化用户体验,推动鸿蒙生态繁荣发展。
832 12
|
Python
Python编程基石:整型、浮点、字符串与布尔值完全解读
本文介绍了Python中的四种基本数据类型:整型(int)、浮点型(float)、字符串(str)和布尔型(bool)。整型表示无大小限制的整数,支持各类运算;浮点型遵循IEEE 754标准,需注意精度问题;字符串是不可变序列,支持多种操作与方法;布尔型仅有True和False两个值,可与其他类型转换。掌握这些类型及其转换规则是Python编程的基础。
828 33
|
开发框架 Dart 前端开发
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
【4月更文挑战第30天】对比 Flutter(Dart,强类型,Google支持,快速热重载,高性能渲染)与 React Native(JavaScript,庞大生态,热重载,依赖原生渲染),文章讨论了开发语言、生态系统、性能、开发体验、学习曲线、社区支持及项目选择因素。两者各有优势,选择取决于项目需求、团队技能和长期维护考虑。参考文献包括官方文档和性能比较文章。
977 0
【Flutter前端技术开发专栏】Flutter与React Native的对比与选择
|
安全 开发工具 Android开发
【Android Git】Git版本回退方式
在实际操作中,选择合适的版本回退方式,可以有效地管理代码版本,提高开发效率和代码质量。
929 26
|
机器学习/深度学习 人工智能 API
GPT-4o-Transcribe:OpenAI 推出高性能语音转文本模型!错误率暴降90%+方言通杀,Whisper当场退役
GPT-4o-Transcribe 是 OpenAI 推出的高性能语音转文本模型,支持多语言和方言,适用于复杂场景如呼叫中心和会议记录,定价为每分钟 0.006 美元。
905 2
|
域名解析 人工智能 API
学生看过来!白嫖阿里云服务器自建DeepSeek个人网站,3分钟站点上线,超简单!
学生看过来!利用阿里云服务器自建DeepSeek个人网站,3分钟快速上线,超简单!学生用户可领取300元代金券,实现0成本部署;普通用户则可选择99元/年的服务器。通过阿里云计算巢“DeepSeek个人站点-快速部署”服务,无需代码,最快5分钟即可体验多个AI模型。教程详情涵盖从打开部署页面、选择套餐、设置实例密码到获取API-KEY的完整步骤,助你轻松搭建专属AI网站。
975 1
|
自然语言处理 前端开发 安全
当被问到你使用过iframe吗?有哪些优点和缺点?
当被问到你使用过iframe吗?有哪些优点和缺点?
934 0

热门文章

最新文章