彻底搞懂 ThreadLocal
作用
多线程环境下,对于同一个变量,每个线程保存自己线程私有的值
明确:ThreadLocal 不是一个 map,而是一个 key,真正的 map 是在 Thread 类持有的 ThreadLocalMap,这个 map 放在了 ThreadLocal 类里面去实现
Thread、ThreadLocal、ThreadLocalMap 的关联如下
常见用法
源码分析
ThreadLocal 类一共暴露出了四个实例方法
ThreadLocal:构造方法,负责生成实例的 hashCode
get:获取此实例中保存的值
set:设置此实例中保存的值
remove:移除此实例中保存的值
set
首先说下 set 的实现原理,大致的流程图如下:
以上最重要的细节没有体现,包括 expungeStaleEntry 和 rehash
由于 ThreadLocalMap 使用线性探测法来解决哈希冲突,因此每次往 map 里面新增数据或者更新已有数据但该数据不在首次哈希索引上时,都会触发一次清理,清理掉 key 为 null 的 entry,并必要时对清理路径上的非 null 值做重新 hash,目的是为了保证下次 get 时可以最大可能在首次哈希索引上就能找到,而不需要探测。
试想一下,如果没有清理和 rehash 的操作,则哈希表就会退化成一个线性查找的数组。
下面用一些实例来说明清理过程:
注意,以上并不是 threadLocal 本身为 null,而是 Entry 持有的 threadLocal 弱饮用被 gc 回收了,或者应用程序在 threadLocal 上面调用了 remove
由于 threadLocalMap 是每个线程私有维护的,会随着线程的释放而被 GC 回收,因此线程的数量跟 threadLocalMap 的容量无关,而是 threadLocal 变量的多少决定了 threadLocalMap 的大小,决定了是否清理和扩容
rehash 过程如下:
先执行一遍完整的清理,把 key 引用为 null 的 Entry 回收掉,不为 null 的重新 hash 放回正确的位置
如果上述清理完成后数组的实际保存的元素大小超过 3/4 的 threshold(即数组大小的一半),则扩容
生成一个 2 倍大小的数组,依次把原数组中的 entry hash 到新数组位置
get
理解了上述 set 的原理之后 get 其实非常简单了
如果线程的 threadLocalMap 还没有初始化,则初始化一个 map,并以 value=null 生成一个 entry 作为初始值
根据 threadLocal 的 hash 值找到数组下标,如果 entry 为 null,则和上面一样以 value=null 生成一个 entry 作为初始值并返回
如果 entry 不为 null,判断 entry 里面的 threadLocal 是否和 key 一样,如果一样则返回 value,否则依次往后找
往后找的过程中如果发现 entry 的 key 为 null 了,则也会主动触发一次清理过程
remove
和 get 同样的方式首先定位 entry,把 entry 的 threadLocal 设置为 null,并触发一次清理
对比 ThreadLocalMap 和 HashMap
相同点
底层都是采用数组实现
都有负载因子和扩容的过程
不同点
HashMap 类似于一个全局容器,根据 key 获取 value;ThreadLocalMap 更像是一个私有变量,获取过程不需要 key,因为 key 就是 threadLocal 变量
HashMap 提供的方法很多,ThreadLocalMap 则只有 3 个方法可以用
哈希冲突的解决方式不一样,HashMap 使用链表和红黑树解决冲突,ThreadLocalMap 使用线性探测法解决哈希冲突
Entry 不一样,HashMap 的 Entry 里面保存了 key 和 value,而 ThreadLocalMap 的 Entry 可以认为是一个只保存了 value 的 threadLocal 的弱引用
ThreadLocalMap 在 get 和 set 过程发生哈希冲突时,会清理掉保存 null 引用的 entry,而 HashMap 不会清理
版权声明: 本文为 InfoQ 作者【千珏】的原创文章。
原文链接:【http://xie.infoq.cn/article/301f9acfaa17252709f7d4105】。文章转载请联系作者。
评论