ThreadLocal是Java提供的一个线程安全类,其原理是每个线程都拥有各自的变量内存副本。其实就是每个线程Thread里都有一个ThreadLocalMap类,用于存储变量值。更新、删除操作时,都是操作各自线程里的hreadLocalMap类,互不影响,从而达到的线程安全
ThreadLocal经常用于一次调用的上下文储存场景,例如一次调用的token、traceId,在调用的各个阶段都有可能用到,但是每次调用的值都不一样,这时ThreadLocal就派上用场了
但是如果ThreadLocal使用不当,会引发内存溢出,其实ThreadLocal的作者Josh Bloch在写ThreadLocal的时候,意识到了这点,例如弱引用等,但是依然会有内存溢出的可能,下面我们来分析一下原因
看源码ThreadLocal底层都是用的Thread里的ThreadLocalMap类,其实ThreadLocal就是一个工具类而已,底层都是操作的Thread里的ThreadLocalMap类。
再看看ThreadLocalMap的源码
当我们使用ThreadLocal.set()时,set的value与key(即业务自己定义的ThreadLocal类)会存储在ThreadLocalMap的Entry[]数组里
其中Entry是实现了一个弱引用WeakReference,Entry的key(即业务方定义的 ThreadLocal类)会被包装成一个弱引用当成Entry的key。Java的弱引用的定义是,当JVM执行垃圾回收扫描的时候,当发现只有弱引用的对象时,会立即回收此对象,这是ThreadLocal当初设计的时候防止内存溢出的一个手段
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
虽然key被包装成了一个弱引用会被垃圾回收机制给回收,但是value在线程(Thread)不死亡时却可能存在一条强引用链.
由于value是强引用,只要Thread不死亡时,例如线程池,这条强引用链就会存在,那么value就不会回收,可能造成内存溢出
虽然ThreadLocal的作者想到了这点,也做了些优化,例如在get的时候当发现key是null的时候,会遍历一次整个Entry数组,remove掉key为null的entry,把value指向null,消除这条强引用链。源码方法为expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
但是这个消除强引用链的动作是需要业务方在get的情况下触发的,可能业务方并不会get、也可能get是key不为空,并不会触发expungeStaleEntry类。所以开发者要养成良好的习惯,记得用完ThreadLocal时,调一次ThreadLocal.remove()方法或者ThreadLocal.set(null)