一、实现原理
通过每个线程维护一张ThreadLocalMap
哈希映射表,key为ThreadLocal
的弱引用,value是Object本身。也就是说,ThreadLocal本身不存任何实际值,而是通过本身作为key,从ThreadLocalMap
中获取具体的值。
实际上,ThreadLocalMap
保存的是Entry
(这个是底层代码实现,每个线程持有)的一个数组,通过ThreadLocal
的hashCode&(len-1)
获取数组的游标i
,具体如下
private void set(ThreadLocal key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
二、内存溢出分析
由于Entry
使用弱引用关联ThreadLocal
对象,而线程堆栈内的是使用强引用关联ThreadLocal
对象,当线程堆栈中的引用释放,遇到下次gc,ThreadLocal
将被释放,也就是说,Entry的key将变成null,但此时实际的Object还是没有被释放,一直在堆内存里面,如果该线程一直没结束,那这块内存就将一直不能被回收。
解决
在ThreadLocal设计中,方法get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是还是无法完全保证内存泄漏问题,例如ThreadLocal声明称类成员静态变量,也就是说ThreadLocal的引用分配在了方法区,这种导致强引用一直会存放在类方法区,无法回收ThreadLocal实例,这种情况可能会导致内存泄漏,具体可以参见ThreadLocal内存泄漏实例
ThreadMap为什么要用弱引用
为什么使用弱引用而不是强引用?
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.(官网)
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
三、实践
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。