[TOC]
概念
ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
示例
1 | public class ThreadLocalTest { |
结果
1 | strLabel = child |
ThreadLocal.set()
ThreadLocal
中的set
方法原理如上图所示,很简单,主要是判断ThreadLocalMap
是否存在,然后使用ThreadLocal
中的set
方法进行数据处理。
代码如下:
1 | public void set(T value) { |
主要的核心逻辑还是在ThreadLocalMap
中的,一步步往下看,后面还有更详细的剖析。
ThreadLocalMap Hash算法
既然是Map
结构,那么ThreadLocalMap
当然也要实现自己的hash
算法来解决散列表数组冲突问题。
1 | int i = key.threadLocalHashCode & (len-1); |
ThreadLocalMap
中hash
算法很简单,这里i
就是当前key在散列表中对应的数组下标位置。
这里最关键的就是threadLocalHashCode
值的计算,ThreadLocal
中有一个属性为HASH_INCREMENT = 0x61c88647
1 | public class ThreadLocal<T> { |
每当创建一个ThreadLocal
对象,这个``ThreadLocal.nextHashCode
这个值就会增长 0x61c88647
。
这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash
增量为 这个数字,带来的好处就是 hash
分布非常均匀。
我们自己可以尝试下:
可以看到产生的哈希码分布很均匀,这里不去细纠斐波那契具体算法,感兴趣的可以自行查阅相关资料。
ThreadLocalMapHash冲突
注明: 下面所有示例图中,绿色块
Entry
代表正常数据,灰色块代表Entry
的key
值为null
,已被垃圾回收。白色块表示Entry
为null
。
虽然ThreadLocalMap
中使用了黄金分隔数来作为hash
计算因子,大大减少了Hash
冲突的概率,但是仍然会存在冲突。
HashMap
中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。
而ThreadLocalMap
中并没有链表结构,所以这里不能适用HashMap
解决冲突的方式了。
如上图所示,如果我们插入一个value=27
的数据,通过hash
计算后应该落入第4个槽位中,而槽位4已经有了Entry
数据。
此时就会线性向后查找,一直找到Entry
为null
的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了Entry
不为null
且key
值相等的情况,还有Entry
中的key
值为null
的情况等等都会有不同的处理,后面会一一详细讲解。
这里还画了一个Entry
中的key
为null
的数据(Entry=2的灰色块数据),因为key
值是弱引用类型,所以会有这种数据存在。在set
过程中,如果遇到了key
过期的Entry
数据,实际上是会进行一轮探测式清理操作的,具体操作方式后面会讲到。
ThreadLocalMap.set()
我们来回顾一下ThreadLocal的set方法可能会有的情况
探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可
探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot
期间会先往前探测
在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry
往后遍历结束,会
1
2// 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍
探测式清理流程
expungeStaleEntries从开始位置向后探测清理过期数据,将过期数据的Entry
设置为null
,沿途中碰到未过期的数据则将此数据rehash
后重新在table
数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null
的桶中,使rehash
后的Entry
数据距离正确的桶的位置更近一些。
探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。
1 | /** |
操作逻辑如下:
1 | private void set(ThreadLocal<?> key, Object value) { |
启发式清理
1 | /** |
ThreadLocalMap.get()详解
根据入参threadLocal的threadLocalHashCode对表容量取模得到index
- 如果index对应的slot就是要读的threadLocal,则直接返回结果
- 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
- 没有找到key,返回null
1 | private Entry getEntry(`ThreadLocal`<?> key) { |
怎么解决内存泄漏
当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决:使用remove
调用remove方法,肯定会删除对应的Entry对象
为什么要用弱引用
那换做强引用分析: ThreadLocal
对象被两个强引用指向
- 强引用: threadlocal1
- 强引用: Entry.key
当我们断开程序中的强引用 threadlocal1
时。ThreadLocal
对象仍然被强引用Entry.key
指向,不会回收,这就造成,ThreadLocal
对象与 value
都成为了脏数据。
弱引用带来哪些问题
不管软引用还是强引用,都可能出现内存泄漏问题,弱引用反而将内存泄漏的程度降低**
利用弱引用的Entry会有key为null这个特征,可以识别哪些是不用的数据,进行清理操作,弱引用 反而提高了ThreadLocal的安全性。事实上当调用ThreadLocal
的get(),set(),reomve()
方法,都会清除掉线程ThreadLocalMap
中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
ThreadLocal可能存在哪些问题?
线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。
ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。
Set的源码
1 | private void set(ThreadLocal<?> key, Object value) { |
- 遍历当前
key
值对应的桶中Entry
数据为空,这说明散列数组这里没有数据冲突,跳出for
循环,直接set
数据到对应的桶中 - 如果
key
值对应的桶中Entry
数据不为空 - 1 如果
k = key
,说明当前set
操作是一个替换操作,做替换逻辑,直接返回 - 2 如果
key = null
,说明当前桶位置的Entry
是过期数据,执行replaceStaleEntry()
方法(核心方法),然后返回 for
循环执行完毕,继续往下执行说明向后迭代的过程中遇到了entry
为null
的情况- 1 在
Entry
为null
的桶中创建一个新的Entry
对象 - 2 执行
++size
操作 - 调用
cleanSomeSlots()
做一次启发式清理工作,清理散列数组中Entry
的key
过期的数据 - 1 如果清理工作完成后,未清理到任何数据,且
size
超过了阈值(数组长度的2/3),进行rehash()
操作 - 2
rehash()
中会先进行一轮探测式清理,清理过期key
,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑(扩容逻辑往后看)