注意:
看这篇文章之前得对 ThreadLocal 有个大致的了解,不然看起来还是蛮吃力的。
先说结果,内存泄漏的确是因为弱引用引起的,为什么呢?
先看下 ThreadLocal 的 set 方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
createMap 如下:
void createMap(Thread t, T firstValue) {
// t 指的是当前线程,即调用 threadLocal.set方法所在的线程
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
可以看出ThreadLocal 的数据是存在 ThreadLocalMap,而 ThreadLocalMap 是线程Thread中的变量,所以可以做到线程的数据隔离。
ThreadLocalMap 中是以数组的形式存放数据 private Entry[] table,至于插入数据时产生的碰撞这里不作说明了,可能下一篇会说明。Entry 的数据结构如下:
static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); // key 为 ThreadLocal的弱引用 value = v; } }
我们把上面的引用链画下来,如下:
其中,虚线表示弱引用。
下面来说下为什么弱引用会导致内存泄漏?
一般项目中使用线程都是利用线程池,而线程池中的线程可能一直存活,这就导致了当外部不再持有 ThreadLocal的引用的时候,发生GC,导致 ThreadLocal 对象被回收了。
当 Entry 中的 key 即 ThreadLocal 对象被回收了之后,会发生 Entry 中 key 为 null 的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread->ThreadLocalMap ->Entry 这条强引用在,这样没用的内存无法被回收就是内存泄露。
当然,如果不是线程池使用方式的话,其实不用关系内存泄漏,反正线程执行完了就都回收了,但是一般我们都是使用线程池。
但是,没关系,在作者设计 ThreadLocal 的时候已经考虑到这种情况了,所以在 ThreadLocal 的 set、get、以及扩容的时候都会清理 key = null 的数据。但是最佳实践是当不再使用的时候,手动 remove 掉,如下:
void dosth {
threadlocal.set("1234");
try {
// do sth ...
} finally {
threadlocal.remove();
}
}
既然弱引用会导致内存泄漏,那为什么不用强引用呢?
同样的,如果是强引用,如果线程一直在,那么就算外部不再持有 ThreadLocal 的引用,但是会存在如下的引用链 GCRoots -> 线程对象 -> ThreadLocalMap -> Entry -> Key(ThreadLocal),导致ThreadLocal 对象得不到回收。
看到这里,可能有人会说那线程被回收之后就好了呀。
重点来了!线程在我们应用中,常常是以线程池的方式来使用的,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!
可以通过如下的方法来看看弱引用的 ThreadLocal 是否被回收。
ThreadLocalthreadLocal = new ThreadLocal<>(); threadLocal.set("我是在主线程中设置的值"); // 如果不设置为null,那么由于有强引用持有 threadLocal,那么不会被回收 threadLocal = null; // 断开 ThreadLocal 的强引用 System.gc(); // 主动垃圾回收 Thread curThread = Thread.currentThread(); Class extends Thread> clz = curThread.getClass(); Field field = null; try { field = clz.getDeclaredField("threadLocals"); field.setAccessible(true); Object threadLocalMap = field.get(curThread); Class> tlmClass = threadLocalMap.getClass(); Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); Object[] arr = (Object[]) tableField.get(threadLocalMap); for (Object o : arr) { if (o == null) continue; Class> entryClass = o.getClass(); Field valueField = entryClass.getDeclaredField("value"); Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); valueField.setAccessible(true); referenceField.setAccessible(true); System.out.println(String.format("弱引用key:%s 值:%s", referenceField.get(o), valueField.get(o)) + " , thread >> " +Thread.currentThread().getName()); } } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); }



