ThreadLocal 系列:
首先来看两个概念:内存溢出、内存泄漏
- 内存溢出: Memory overflow,没有足够的内存提供申请者使用
- 内存泄漏: Memory Leak,程序中已经动态分配的堆内存由于某种原因, 程序未释放或者无法释放, 造成系统内部的浪费, 导致程序运行速度减缓甚至系统崩溃等严重结果. 内存泄漏的堆积终将导致内存溢出
我们下面就来看看 ThreadLocal 为什么会出现内存泄漏问题。

图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于 Stack(栈)区,而普通的对象是存储在Heap(堆)区。
- 线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
- 当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化
- Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作
- 图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用
PS:关于对象的几种引用类型
- 强引用:只要强引用还在,该引用指向的实例对象就永远不会被回收
- 弱引用:弱引用指向的对象只能存活到GC之前,需要继承 WeakReference
- 软引用:软引用指向的对象会在内存不足的时候被回收
那么,ThreadLocal 内存泄漏的问题是因为软引用么?假设 ThreadLocalMap 中的key使用了强引用, 那么会出现内存泄漏吗?
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 使用了弱引用, 那么会出现内存泄漏吗?
假设在业务代码中使用完 ThreadLocal, ThreadLocal ref 被回收了。
由于 threadLocalMap 只持有 ThreadLocal 的弱引用,没有任何强引用指向 threadlocal 实例(这里 Entry 不再强引用 ThreadLocal了), 所以 threadlocal 就可以顺利被 gc 回收, 此时 Entry 中的 key = null。
在没有手动删除 Entry 以及 CurrentThread 依然运行的前提下,也存在始终有强引用链 CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry。
所以,value 就不会被回收,而这块 value 永远不会被访问到了(因为key=null), 导致value内存泄漏。
结论
内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的,或者说无论是使用强引用或者软引用都无法避免泄漏。
2.内存泄漏的真实原因
我们可以发现,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动侧除这个 Entry
- CurrentThread 当前线程依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。
结论
ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏。
3.避免内存泄漏
要避免内存泄漏有两种方式:
- 使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
- 使用完 ThreadLocal ,当前 Thread 也随之运行结束
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。
另外,相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
PS: 到这里我们再来看为什么 ThreadLocal 中使用的是弱引用:因为弱引用的 ThreadLocal 会被回收,所以就算忘记调用 remove 方法,在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏。
也就是说,只要记得在使用完 ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题。






还没有评论,来说两句吧...