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/