ThreadLocal是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
提供了set和get方法来访问拷贝过来的变量副本。底层也是封装了ThreadLocalMap集合类来绑定当前线程和变量副本的关系,各个线程独立并且访问安全。
ThreadLocal 存储结构
每个线程持有自己的 ThreadLocalMap,ThreadLocalMap 初始容量为16,在调用ThreadLocal 的 set 方法时,将以ThreadLocal的弱引用作为Key存储在本线程的ThreadLocalMap 里面,ThreadLocalMap的Value为Object 类型。ThreadLocal的set() 方法、get() 方法以及remove() 方法,最终都落到了ThreadLocalMap的头上。
ThreadLocalMap 是ThreadLocal 内部的一个Map实现,数据结构采用数组 + 开放地址法。ThreadLocalMap的Entry继承WeakReference,以ThreadLocal的弱引用为key。虽然继承 WeakReference,但只能实现对 Reference 的 key 的回收,而对 value 的回收需要手动解决。当 map.get() = null 的时候将它称为 过期。
ThreadLocalMap 之 key 的 hashCode 计算
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
//1640531527 这是一个神奇的数字,能够让hash槽位分布相当均匀
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
在ThreadLocalMap中的hashCode全部使用 threadLocalHashCode 字段。threadLocalHashCode 用 final修饰,不可变。threadLocalHashCode 的生成调用 nextHashCode(),所有ThreadLocalMap 的 hashCode 使用静态的 AtomicInteger 每次增加1640531527 来产生。ThreadLocal 的 nextHashCode 是由 static 修饰的,他是一个共享变量,所有的 ThreadLocal 共享一个 AtomicInteger,在其基础上 CAS 增加。
ThreadLocalMap 之 set() 方法
set 方法包含:开放地址法;hash算法:均匀的 hash 算法使其可以很好地配合开放地址法使用以及过期值清理
ThreadLocalMap 为什么采用开放地址法?
由于 ThreadLocalMap 的 hashCode 的精妙设计,使hash冲突很少,并且 Entry 继承 WeakReference, 很容易被回收并且开放地址可以节省一些指针空间。
对于过期值清理:
cleanSomeSlots
当新元素被添加时或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry,寻找过期的条目。它执行对数数量的扫描,此方法并没有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面。
expungeStaleEntry(int staleSlot)
这里是真正的清除,不仅仅会清除当前过期的slot,还会往后查找直到遇到null的slot为止。
ThreadLocalMap 之 getEntry() 方法
getEntry() 主要是在 ThreadLocal 的 get() 方法里被调用
首先运算槽位 i ,然后判断 table[i] 是否是目标entry,不是则进入 getEntryAfterMiss(key, i, e);getEntryAfterMiss方法是在遇到 hash 冲突时往后继续查找,并且会清除查找路上遇到的过期slot。
ThreadLocalMap 之 rehash() 方法
rehash() 里首先调用 expungeStaleEntries(),然后循环调用 expungeStaleEntry(j),此方法会清除所有过期的slot。继续看 resize():resize() 方法里也会过滤掉一些过期的 entry。
总结ThreadLocalMap 的 value清理触发时间:
ThreadLocalMap使用ThreadLocal的弱引用
作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value就无法访问,但是value却存在一条从Current Thread过来的强引用链。因此,这个线程对象被GC回收,那些key为null对应的value才会被回收,但在线程对象不被回收的情况下,核心线程是一直在运行的,就可能出现内存泄露的问题。
ThreadLocalMap的解决方案是在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal> key)
方法。通过key.threadLocalHashCode & (table.length - 1)
来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal>, int, Entry)
方法。ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)
方法,此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。
在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果既不需要添加value,也不需要获取value,还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()
函数,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。
其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。
Java中常用的开源框架,Mybatis、Hibernate中涉及到线程通过数据库连接对象Connection,对其数据进行操作,都会使用ThreadLocal类来保证Java多线程程序访问和数据库数据的一致性问题。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等