一直以来认为ThreadLocal只是简单的分装了一下HashMap,使用线程作为key来存储。这样也符合我们的习惯思维。需要存储多少线程变量就创建多少ThreadLocal。
及如下图这样,及通过对HashMap实现简单的封装就直接使用;
package gy.mao;
import java.util.HashMap;
import java.util.Map;
publicclassMyThreadLocal {
Map map =null;
public MyThreadLocal() {
}
publicvoid set(T value) {
if(map ==null) {
map =newHashMap<>();
}
map.put(Thread.currentThread(), value);
}
public T get(){
if(map ==null) {
thrownewRuntimeException("还未初始化");
}
return map.get(Thread.currentThread());
}
publicint size(){
if(map ==null) {
thrownewRuntimeException("还未初始化");
}
return map.size();
}
}
然而今天仔细的看了看却发现他实际是这样的
问题一、为什么实际的ThreadLocal却不是我们通常所理解的这样呢?
这就涉及到我们常说的内存泄露的问题。
通过简单封装HashMap的方式,如果不手动remove()线程所放入的Entry。那么由于ThreadLocal一直存在,导致该Entry将一直持有,不会被垃圾回收器回收。随着线程不断的创建,put数据,必然会内存溢出。
问题二、可是不都说使用的ThreadLocal也是需要remove() 才行,要不然也会内存溢出啊
在回答这个问题前,先做个测试
首先是使用我自己实现的MyThreadLocal测试
然后是使用原生的ThreadLocal测试
两个程序实现一样的功能,都是不断的创建线程,然后往ThreadLocal中存数据。
可以很明显的观察到,使用我自己实现的MyThreadLocal最终会导致内存溢出,并且手动触发垃圾回收也无济于事;这也很符合我们的预期。
可是使用原生的ThreadLocal及时在没有使用remove的情况下,也没有内存溢出,而且垃圾回收还很规律的在进行。
问题三、ThreadLocal不使用remove()会有内存泄露的问题?
一起来简单看下源码,set()方法,
首先是调用了getMap方法及通过当前线程去获取ThreadLocalMap(再深入可以看到该ThreadLocalMap实际是属于当前线程所有),
没有则创建,有则直接set值进去(注意此时的set的值为this,及当前ThreadLocal)。
再来看下ThreaLocalMap,可以很明显的看到里面的Entry继承了弱引用
及ThreadLocal的真正的实现是这样的(图片来源于网络)
因此可以看出,真正的ThreadLocal的哲学为:一个线程维护一个ThreaLocalMap,多个线程变量ThreadLocal存放在这个ThreadLocalMap中,ThreadLocal以弱引用的方式作为ThreadLocalMap的key。
所谓的不remove()会导致内存溢出是: 由于ThreadLocalMap的Entry中key是弱引用,当ThreadLocal不再被使用及会被垃圾回收器回收,但是value 由于是被Entry强引用,而Entry又是被ThreadLocalMap强引用,而ThreadLocalMap又是属于当前线程的。这样就形成key已经被回收了,但是Value一直不能回收,且不会再次使用到。
问题四、前面的实验中使用ThreadLocal没有导致内存泄露的原因是?
仔细看代码就知道,代码中每次都是创建线程之后就往ThreadLocal中放数据,而放完数据之后线程马上就结束了。因此对应的ThreadLocal,依附于Thread的ThreadLocalMap都是可以被垃圾回收的,及皮将不存,毛之焉附。而不remove会到导致内存泄露是线程还在继续运行的情况。
问题五、为啥ThreadLocalMap的key是弱引用而不是正常的强引用呢?
通过上面的源码分析,我门已经知道ThreadLocalMap是数据线程对象的,而如果ThreadLocalMap的key为强引用,则当线程运行结束回收ThreadLocalMap对象时,由于存在ThreadLocal对ThreadLocalMap对象的强应用将导致ThreadLocalMap对象无法被回收,及线程已经结束,但线程对象ThreadLocalMap却没有办法被回收,造成内存泄漏。
此外,ThreadLocal没有采用我锁实现的MyThreadLocal这种实现思想的一个重要原因是锁的问题。
众所周知HashMap有线程安全的问题,如果要使用MyThreadLocal的这种思想必然得考虑该问题,得使用锁来控制,这无疑增加了性能损耗和复杂度。
而反观系统提供的ThreadLocal,由于是一个线程维护一个ThreadLocalMap数据,从设计上已经避免了多线程带来的安全问题。
总结
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
参考:https://zhuanlan.zhihu.com/p/102571059