Thread local原理梳理

Thread local参考 https://www.cnblogs.com/micrari/p/6790229.html
线性探测法参考https://www.cnblogs.com/-beyond/p/7726347.html

1 场景
不加锁的情况下,多线程安全访问共享变量,每个线程保留共享变量的副本(线程特有对象),每个线程往这个ThreadLocal中读写是线程隔离。

2 原理
2.1 ThreadLocal方法详解:
ThreadLocal的get\set\remove方法均先找出当前线程的ThreadLocalMap,然后执行ThreadLocalMap的get\set\remove,每个线程均有自己的ThreadLocalMap,ThreadLocalMap的Entry是ThreadLocal的弱引用,Entry成员变量value为当前线程ThreadLocal的线程特有对象,形成类似K-V的结构。
每个线程调用ThreadLocal的get\set\remove方法时,均在本线程对象的ThreadLocalMap中操作,所以实现了线程隔离。
ThreadLocalMap是惰性构造的,如果get\set没有ThreadLocalMap,均先创建ThreadLocalMap,get创建ThreadLocalMap的key为ThreadLocal,v为初始化的值;set创建ThreadLocalMap的初始值即为指定的k和v
如果get方法有ThreadLocalMap,但是没有值,会先调用ThreadLocalMap的set方法,key为ThreadLocal,v为初始化的值,然后返回v

2.2 ThreadLocalMap详解
ThreadLocalMap是ThreadLocal的核心。
ThreadLocalMap维护了Entry环形数组,Entry是ThreadLocalMap里定义的节点,ThreadLocalMap的Entry是ThreadLocal的弱引用,Entry成员变量value为当前线程ThreadLocal的线程特有对象(塞到ThreadLocal里的值),就是,形成类似K-V的结构。
弱引用的作用
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定(key和value一直被线程强引用),只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的引用变成null,基于此可以进行节点清理,这为ThreadLocalMap本身的垃圾清理提供了便利。
为什么Entry数组大小必须为2的幂
这和hash函数相关,基于ThreadLocal特有的hash函数,可以使entry在Entry数组上均匀分布,减少hash冲突。

2.2.1 Get方法
(1)根据入参threadLocal的threadLocalHashCode对表容量取模得到index,如果index对应的slot就是要读的threadLocal,则直接返回结果;
(2)否则调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
没有找到key,返回null
段清理:从指定位置开始直到entry为null,将无效的entry去掉,然后对剩下的有效entry重新hash放到合适的位置,保证下次能找到。

2.2.2 Set方法
(1)探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可;
(2)探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot,有以下两种情况:

  • 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值,然后做一次段清理(expungeStaleEntry)和启发式清理(cleanSomeSlots)(while ((n >>>= 1) != 0 ,如果发现新的无效entry,重置n,反复向后扫描执行)。
  • 在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry,然后做一次段清理(expungeStaleEntry)和启发式清理(cleanSomeSlots)(while ((n >>>= 1) != 0 ,如果发现新的无效entry,重置n, 反复向后扫描执行)
    (3)探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理(while ((n >>>= 1) != 0 ,如果发现新的无效entry,重置n),反复向后扫描执行),如果没清理出去key,并且当前table大小已经超过阈值(threshold)了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果全量清理完之后table大小超过了threshold - threshold / 4,则进行扩容2倍。
    启发式清理中包含多次段清理。

2.2.3 Resize
新数组为原来的两倍,遍历原来不为null的entry,如果entry的引用为null,则释放value的强引用,否则基于线性探测法存放entry.

2.2.4 Remove方法
remove方法相对于getEntry和set方法比较简单,直接在table中找key,如果找到了,把弱引用断了做一次段清理。

3 Hash冲突的处理
ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。注意如果删除一个节点后,需要将后面的节点重新hash。

4 扩容实现
新数组为原来的两倍,遍历原来不为null的entry,如果entry的引用为null,则释放value的强引用,否则基于线性探测法存放entry。

5 内存泄漏
之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。
只有在get的时候才会第一次创建初始值,所以用完后使用remove,可以将这个entry去掉,下次get还会重新加载,这样避免了内存泄漏。
Get和set方法执行时,偶尔发现无效entry后做段清理,可能清理不完全,导致可能存在大对象滞留。
如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。

7 其他
取余运算 在计算商值时,商值向0方向舍入;
取模运算 在计算商值时,商值向负无穷方向舍入;

上一篇:数据结构队列的设计 (单向链表的应用)


下一篇:Java中Map的entrySet()详解以及用法(四种遍历map的方式)