ThreadLocalMap 简单分析
ThreadLocal
在使用多线程的时候,如果需要保存一份线程自己私有的一部分变量,避免其他线程污染这个变量的话,一般都会自己手动 new 一个 ThreadLocal,如下代码:
1 | ThreadLocal<String> threadLocal = new ThreadLocal<>(); |
当需要在某一刻使用这个变量的时候,只需要手动调用下
1 | threadLocal.get(); |
在这里可以看到的是,ThreadLocalMap.Entry 是以 ThreadLocal<?>
为 key 的,也就是 ThreadLocalMap 它的整体设计如下:
变量
table
:表示的是一个 Entry
数组threshold
:下一次扩容的临界值,算法为 数组的长度的 2/3INITIAL_CAPACITY
:数组的大小,默认是 16
Entry扩容
table
的扩容是直接新建了一个 Entry[]
数组,将其长度设置为旧数组长度的 2 倍,然后通过一个 for
循环,将元素都重新移至新的 table
上,但是在移植的过程中,如果发现了 Entry
中的 Key 为空的话,那么就会直接将其 value 设置为 null 来帮助GC,避免内存的泄漏。
如果在移植的过程中发生了 Hash 碰撞,那么会直接将当前下标 + 1,然后判断该位置是否有元素,如果有的话,继续 + 1,直至没有元素。
新增过程
当调用 threadLocal.set()
方法的时候,会首先判断 ThreadLocalMap
存不存在。从而进入不同的处理流程上来。
ThreadLocalMap 不存在
如果不存在的话,就会调用 createMap
方法来进行初始化。
而 createMap
直接就是通过默认值来初始化一个 ThreadLocalMap
。
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
ThreadLocalMap 存在
如果存在的话,这时直接以当前的 ThreadLocal
为 Key
,通过 Hash算法
算出该对象在 table
中的下标,然后判断该下标是否有值(Entry是否为null),如果有值的话,则判断 Entry
中的 key
是否与当前的 ThreadLocal
相等(地址比较),如果相等的话,则直接将 value
赋值,然后 return。
如果 Entry
中的 key
为 null
的话,则会执行 replaceStaleEntry
方法来找 key
,同时在这个方法内部,还会通过 渐进式或者启发式
的方式来进行清除旧key操作。
先大致列出其 set 的过程:
1. 获取该 key 的 threadLocalHashCode
,然后与当前 table(Entry[]的长度) -1
进行 &
运算,计算出下标。
Hash算法
ThreadLocalMap
的 Hash算法
采用的是 斐波那契散列
,其过程是用 0x61c88647
累加,然后用累加的结果与当前 table(Entry[]的长度) - 1
进行 &
运算。
0x61c88647
转化为十进制是1640531527
,
1 | private static AtomicInteger nextHashCode = new AtomicInteger(); |
在这里的 nextHashCode
是一个 AtomicInteger
,因此可以保证其原子性,而且又是私有的静态变量,所以可以尽量保证每一个 ThreadLocal
都会得到一个唯一的 HashCode
。
2. 找出 table[i]
中不为 null
的 Entry
,在这个过程中会一直 i+1
,如果 i+1
大于数组的最大下标的话,则直接从 0 开始寻找。
3. 当在遍历的过程中,如果发现 Entry 的 key
与当前的 ThreadLocal
对象相等的话,则直接将值替换,如果发现某一个 Entry
的 key
为 null 的话,则直接进行 replaceStaleEntry
。然后return。
replaceStaleEntry方法
该方法出现在 set方法
的第三步,即当判断到 Entry
中的 key 为 null
,那么此时就会调用 replaceStaleEntry
来清除那些被回收了的 Key
。在这个方法里面,每一个遍历都会将 staleSlot
赋值到 i
。
1 | private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { |
4. 在第三步的时候,直接找到了为 null
的Entry
,则直接新建一个 Entry
然后赋值,
5. 当上述步骤都处理完了以后,会通过 cleanSomeSlots
进行判断,如果有 Entry
的弱引用是否被回收了,则会进行 rehash
,除非数组的长度已经达到了 threshold
并且未发现 Entry
的弱引用被回收了,才会进行 rehash。
1 | tab[i] = new Entry(key, value); |
set方法流程图
expungeStaleEntry(int staleSlot)方法
该方法是 ThreadLocal
主要用于清除旧 Key,其代码如下:
1 | private int expungeStaleEntry(int staleSlot) { |
在这里之所以是要进行重新
hash
,是因为一旦出现某一个Key
被回收以后,会导致后面的Key
无法正确的hash
到数组的正确下标下,从而导致每一次的get
操作都会进行一次遍历,时间复杂度由O(1)
直接退化为O(n)
Get方法
ThreadLocal
的 get
方法如下:
1 | public T get() { |
在这里需要注意的一个方法是 getEntry
,如果在调用的时候,发生了 Hash冲突
,那么该方法会调用getEntryAfterMiss
。
1 | private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
在这里可以发现,其实最终还是调用的 expungeStaleEntry
,而该方法在之前已经说过了,所以 ThreadLocal
在 get
的时候,其实还会旧的 key
的清除。直至遍历到第一个 e == null
的对象,此时则表示该 key
不存在,于是直接返回 null
。
总结
总的来说,ThreadLocal
在 set
的过程中,首先会进行 Hash
找出下标,如果该下标的 Entry
为 null
的话,则直接赋值,如果不为 null
的话,则会进行遍历,直至找出第一个不为 null
的 Entry
然后赋值。
如果在遍历的过程中,发现了某些 Entry
的 Key
为 null
的话,则代表可以通过清理旧的 Entry
来进行赋值操作,那么其过程是,首先获取到在遍历过程中 Entry
的 Key
为 null
的下标,记为 staleSlot
,然后向前遍历,直至第一个 Entry
为 null
止,然后记录在遍历的过程中,最后一个 Entry
的 Key
为 null
的下标。
随后进行第二次的遍历,如果在往后的遍历过程中,出现了 Entry
的 key
与当前的 key
相等的话,则直接赋值。
然后判断如果在之前的第一个遍历中,所有的 Entry
的 key
都不为 null
,那么此时直接将当前下标赋值为 旧Key
清除的起点,随后先进行一个渐进式清理expungeStaleEntry
,等这一步清理完毕以后,再进行一次启发式清理cleanSomeSlots
,cleanSomeSlots
会进行一次 log2(n)
次清理,以渐进式清理expungeStaleEntry
后的 index
为起点,在之后的 log2(n)
下标内,如果还是出现了 key
为 null
的 Entry
,则还是会再进行 渐进式清理expungeStaleEntry
。
ThreadLocalMap 简单分析
https://somersames.github.io/2020/07/02/ThreadLocalMap-simple-analysis/