ThreadLocal 内存泄露的原因及处理方式
1、ThreadLocal 使用原理
前文我们讲过 ThreadLocal 的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个 ThreadLocal, 但是实际上使用的值 value 却是自己独有的一份。用一图直接表示 threadlocal 的使用方式。
图 1
从图中我们可以当线程使用 threadlocal 时,**是将 threadlocal 当做当前线程 thread 的属性 ThreadLocalMap 中的一个 Entry 的 key 值,实际上存放的变量是 Entry 的 value 值,我们实际要使用的值是 value 值。**value 值为什么不存在并发问题呢,因为它只有一个线程能访问。threadlocal 我们可以当做一个索引看待,可以有多个 threadlocal 变量,不同的 threadlocal 对应于不同的 value 值,他们之间互不影响。ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
2、ThreadLocal 内存泄露的原因
Entry 将 ThreadLocal 作为 Key,值作为 value 保存,它继承自 WeakReference,注意构造函数里的第一行代码 super(k),这意味着 ThreadLocal 对象是一个「弱引用」。可以看图 1.
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
主要两个原因
1 . 没有手动删除这个 Entry
2 . CurrentThread 当前线程依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以 ThreadLocalMap 的生命周期跟 Thread 一样长。如果 threadlocal 变量被回收,那么当前线程的 threadlocal 变量副本指向的就是 key=null, 也即 entry(null,value), 那这个 entry 对应的 value 永远无法访问到。实际私用 ThreadLocal 场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的 entry(null,value) 出现,从而导致内存泄露。
综上, ThreadLocal 内存泄漏的根源是:
由于 ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove() 方法)对应 key 就会导致 entry(null,value) 的对象越来越多,从而导致内存泄漏.
3、 为什么不将 key 设置为强引用
3.1 、key 如果是强引用
那么为什么 ThreadLocalMap 的 key 要设计成弱引用呢?其实很简单,如果 key 设计成强引用且没有手动 remove(),那么 key 会和 value 一样伴随线程的整个生命周期。
1、假设在业务代码中使用完 ThreadLocal, ThreadLocal ref 被回收了,但是因为 threadLocalMap 的 Entry 强引用了 threadLocal(key 就是 threadLocal), 造成 ThreadLocal 无法被回收。在没有手动删除 Entry 以及 CurrentThread(当前线程) 依然运行的前提下, 始终有强引用链 CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)→ entry, Entry 就不会被回收 ( Entry 中包括了 ThreadLocal 实例和 value), 导致 Entry 内存泄漏也就是说: ThreadLocalMap 中的 key 使用了强引用, 是无法完全避免内存泄漏的。请结合图 1 看。
3.3 那么为什么 key 要用弱引用
事实上,在 ThreadLocalMap 中的 set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用 threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应 value 在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.
3.4 如何正确的使用 ThreadLocal
1、将 ThreadLocal 变量定义成 private static 的,这样的话 ThreadLocal 的生命周期就更长,由于一直存在 ThreadLocal 的强引用,所以 ThreadLocal 也就不会被回收,也就能保证任何时候都能根据 ThreadLocal 的弱引用访问到 Entry 的 value 值,然后 remove 它,防止内存泄露
2、每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。