ThreadLocal内存泄露原理
使用方法
ThreadLocal 的使用方法非常简单,声明 ThreadLocal 对象,一般是全局变量,然后调用其 get()、set()、remove() 即可。
如果你想给 ThreadLocal 的 get() 方法增加默认值逻辑,可以通过下面两个方式:
1、继承 LocalThread 类重写 initialValue() 方法:
public class Local extends ThreadLocal<Object> {
@Override
protected Object initialValue() {
return new Date();
}
}
2、调用 ThreadLocal 的静态方法 withInitial,它帮我们重写了 initialValue() 方法:
public class Test {
ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(new Supplier<Object>() {
@Override
public Object get() {
return null;
}
});
}
源码分析
ThreadLocal 的源码非常简单,只需要从 get()、set()、remove() 三个方法开始即可。
set方法
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程对象
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null)
map.set(this, value); // k 为当前 ThreadLocal 对象
else
createMap(t, value); // 初始化 ThreadLocalMap 并赋值
}
看完这段代码的感受就是 ThreadLocal 有点工具类的味道,真正存储 value 的 ThreadLocalMap 是 Thread 的内部属性。
ThreadLocal 其实并没有对 value 有任何引用。
让我们进入 ThreadLocalMap 的 set() 方法内部,它里面的代码才是比较有意思的地方。
private void set(ThreadLocal<?> key, Object value) {
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(); // 获取 Entry 的 k 的内存索引
if (k == key) { // 如果 Entry 的 k 与 key 相同,即同一个 ThreadLocal 对象在调用
e.value = value;
return;
}
if (k == null) { // 有可能存在 k 等于 null 的情况,比如之前的 ThreadLocal 对象被回收了
replaceStaleEntry(key, value, i); // 这一步会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get方法
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程对象
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 当前 ThreadLocal 对象作为 k,取到 v
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 执行 initialValue 方法并构建 ThreadLocalMap 放入当前线程
}
如果没有调用过 set() 方法,那么第一次调用 get() 方法会执行 setInitialValue() 方法初始化 ThreadLocalMap 对象。
让我们再次进入 ThreadLocalMap 的 getEntry() 方法:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) // Entry 的 k 与 key 相同,即同一个 ThreadLocal 对象在调用
return e;
else
return getEntryAfterMiss(key, i, e);
}
如果通过 key 没找到 v,那么 getEntryAfterMiss() 会怎么做呢?
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) { // 如果 e 为 null,证明确实没有 set 过,返回 null 即可
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // set 过,但是之前的 ThreadLocal 对象被 GC 回收了
expungeStaleEntry(i); // 这一步会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); // 获取当前线程的 ThreadLocalMap 属性
if (m != null)
m.remove(this); // 当前 ThreadLocal 对象作为 k
}
remove() 还是比较简单的,再看下 ThreadLocalMap 的 remove() 方法:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i); // 虽然前面有 if 语句,但这个方法依旧会帮忙释放 ThreadLocal 被 GC 回收后未释放的 value 们
return;
}
}
}
总结梳理
看完源码,相信你也晕了,不过不要紧,有图帮你梳理:
回到本文的主题,咱们边看图边总结一下 ThreadLocal 内存回收相关的结论:
-
当 ThreadLocal == null 时,ThreadLocal 由于被 WeakReference 包裹,所以 ThreadLocal 会被 GC 回收。
-
当 Value == null && ThreadLocal !=null 时,value 由于还被当前线程引用着,所以只要当前线程没有 exit() 退出 或者 ThreadLocal 没有调用 remove() 方法移除 Value, Value 就不会被 GC 回收。这就是造成内存泄露的原因。
-
如果 Thread 执行完 Runnable 后就退出,并且 Value == null 成立,Value 会被 GC 回收。 但由于现在都在使用线程池管理线程,线程执行完 Runnable 后并不会退出,并且 ThreadLocal 也大都是 final 修饰的全局变量,所以在使用 ThreadLocal 时,记得调用 remove() 方法防止 Value 内存泄露。
最后一个问题,ThreadLocalMap.Entry 的 Key(即 ThreadLocal)使用弱引用的原因:
如果没有调用 ThreadLocal 的 remove() 方法,那么当 Value == null && ThreadLocal == null 成立时,当前线程也没有退出的话,会存在内存泄露。但当其它 ThreadLocal 对象在同一个线程调用 get()/set()/remove() 方法时,会检测出 ThreadLocal 被回收的 Entry(即 e.get() == null),并将 e.value 释放。(PS:如果 ThreadLocal 在你的项目里是 final 修饰的全局变量,那这一步就挺鸡肋的。)
版权声明:凡未经本网站书面授权,任何媒体、网站及个人不得转载、复制、重制、改动、展示或使用本网站的局部或全部的内容或服务,或在非本网站所属服务器上建立镜像。如果已转载,请自行删除。同时,我们保留进一步追究相关行为主体的法律责任的权利。我们希望与各媒体合作,签订著作权有偿使用许可合同,故转载方须书面/邮件申请,以待商榷。