ThreadLocal造成内存泄露的原因
内存溢出,指的是程序在运行过程中试图分配更多内存,但系统已经没有足够的内存可供分配。
内存泄露,是指程序在申请内存后无法释放不再使用的内存空间。
内存泄露是程序问题,合理分配和使用资源。内存溢出是内存管理问题。
ThreadLocal的实现原理是每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
ThreadLocal的结构
根据源码看下ThreadLocal的整个结构
ThreadLocal<String> threadLocal = new ThreadLocal<>();
String str = threadLocal.get();
/**
* 返回当前线程对该线程局部变量副本中的值。
* 如果该变量在当前线程中尚未赋值,则首先初始化为通过调用{@link #initialValue}方法所返回的值。
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 获取当前线程的引用 t=CurrentThreadRe
Thread t = Thread.currentThread();
// 从当前线程中取出 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 初始化
return setInitialValue();
}
首先分析下ThreadLocalMap,既然定义为xxMap,那数据格式一般键值对存储结构,
根据 map.getEntry(this); 的用法,可以分析出键值对的key 为this,this指的当前ThreadLocal对象。而值是ThreadLocalMap.Entry,Entry中有一个value属性,这个value也是最终return的内容。
不看代码,推理一下逆向set过程
1.获取当前线程
2.从当前线程中取出ThreadLocalMap(==null 则初始化ThreadLocalMap)
3.执行map.getEntry(this); 取出Map中的Entry
4.如果Entry == null,则初始化
5.修改Entry 的value值
验证下源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
既然是Map对象,便会有Hash冲突的风险,ThreadLocalMap使用线性探测来解决。
扩容问题
ThreadLocalMap初始化容量为16,当元数量超过阈值后,会进行x2扩容。
int newLen = oldLen * 2;
一般情况下 这个行为并不会很频繁。ThreadLocalMap对象会一直伴随着Thread的整个生命周期,扩容后会一直保持这个大小。
内容泄露问题
内存泄露,是指程序在申请内存后无法释放不再使用的内存空间。
通常情况下,当线程执行完毕或ThreadLocal实例不再被任何强引用引用时,其关联的线程局部变量应该可以被回收。然而由于ThreadLocalMap的内部实现中键是弱引用,当外部对该ThreadLocal实例没有强引用,键是可以被垃圾回收的,但对应的值却是强引用,这个值不会被垃圾回收器回收。
如上图所示,出现了一些变量 ThreadLocalRef、CurrentThreadRef、CurrentThread、ThreadLocalMap、ThreadLocal、Entry、key、value。其中ThreadLocalRef、CurrentThreadRef 是对CurrentThread和ThreadLocal的引用,栈帧返回后会自动被回收。CurrentThread是线程,ThreadLocalMap是Thread的一个属性。
ThreadLocal 存在两个引用,一个强引用,在栈上;一个弱引用,在ThreadLocalMap中。当栈空间被回收后,ThreadLocal只剩下了一个弱引用,如果发生了GC,那ThreadLocal就会被回收。而对应的value则还是强引用,被绑定在ThreadLocalMap.Entry上。jdk1.8之前如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在。jdk1.8之后,虽然操作ThreadLocal是会主动触发清理,但是如果不触发的话,还是会造成内存泄露的风险。
Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value
这里有一个误区,请看下面对ThreadLocal的定义
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
static 关键字,会让人觉得这是一个静态常量,而静态常量属于类元信息,一个线程的值修改后其它线程也会被改。但是如果理解threadLocal后,每个线程都有自己独立的ThreadLocalMap,而threadLocal只是作为一个key,并不能影响value。
FashtThreadLocal
Netty为提升性能而自定义的ThreadLocal实现
FastThreadLocal的数据结构
FastThreadLocal改用InternalThreadLocalMap,其中是用Object[] indexedVariables;作为数据存储。
Q:数组总是如何定位数据的呢?
A:每一个FastThreadLocal都有一个下标,这个Index是全局唯一的,通过物理层保障这个唯一。
FastThreadLocal是如何优化的
使用数组代替hash变,避免hash冲突而造成的额外性能损失,同时index定位也提升了搜索效率。
至于如何解决内存泄露问题,看到FashtThreadLocal的用法就明了了。
FastThreadLocal用法
三件套
FastThreadLocalThread
FastThreadLocalRunnable
FastThreadLocal
选择对应的Factory,便可以创建对应的FastThreadLocalThread,此时FastThreadLocal才能被正确使用。
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
最重要的一步,执行完任务后,
class FastThreadLocalRunnable implements Runnable {
private final Runnable runnable;
private FastThreadLocalRunnable(Runnable runnable) {
this.runnable = ObjectUtil.checkNotNull(runnable, "runnable");
}
@Override
public void run() {
try {
runnable.run();
} finally {
FastThreadLocal.removeAll();
}
}
....
}
就此 主动的完成了删除无用value的过程。
ThreadLocal的销毁是与Thread绑定在一起的,如果一直不进行。而FastThreadLocal的销毁是与Task绑定在一起了。