深入解析ThreadLocal( 九 )


此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal, threadLocal Ref被回收了 。
但是因为threadLocalMap的Entry强引用了threadLocal , 造成threadLocal无法被回收 。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下 , 始终有强引用链 threadRef->currentThread->threadLocalMap->entry , Entry就不会被回收(Entry中包括了ThreadLocal实例和value) , 导致Entry内存泄漏 。
也就是说 , ThreadLocalMap中的key使用了强引用 ,是无法完全避免内存泄漏的 。
(5)如果key使用弱引用
那么ThreadLocalMap中的key使用了弱引用 , 会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用 , 虚线表示弱引用)如下:
同样假设在业务代码中使用完ThreadLocal, threadLocal Ref被回收了 。
由于ThreadLocalMap只持有ThreadLocal的弱引用 , 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收 , 此时Entry中的key=null 。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下 , 也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value, value不会被回收 ,而这块value永远不会被访问到了 , 导致value内存泄漏 。
也就是说 , ThreadLocalMap中的key使用了弱引用 ,也有可能内存泄漏 。
(6)出现内存泄漏的真实原因
比较以上两种情况 , 我们就会发现 , 内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的 。 那么内存泄漏的的真正原因是什么呢?
细心的同学会发现 , 在以上两种内存泄漏的情况中 , 都有两个前提:

  1. 没有手动删除这个Entry
  2. CurrentThread依然运行
第一点很好理解 , 只要在使用完ThreadLocal , 调用其remove方法删除对应的Entry , 就能避免内存泄漏 。
第二点稍微复杂一点 ,由于ThreadLocalMap是Thread的一个属性 , 被当前线程所引用 , 所以它的生命周期跟Thread一样长 。 那么在使用完ThreadLocal的使用 , 如果当前Thread也随之执行结束 , ThreadLocalMap自然也会被gc回收 , 从根源上避免了内存泄漏 。
综上 , ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长 , 如果没有手动删除对应key就会导致内存泄漏 。
(7) 为什么使用弱引用
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏 , 跟使用弱引用没有关系 。
要避免内存泄漏有两种方式:
  1. 使用完ThreadLocal , 调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal , 当前Thread也随之运行结束
相对第一种方式 , 第二种方式显然更不好控制 , 特别是使用线程池的时候 , 线程结束是不会销毁的 。 也就是说 , 只要记得在使用完ThreadLocal及时的调用remove , 无论key是强引用还是弱引用都不会有问题 。 那么为什么key要用弱引用呢?
事实上 , 在ThreadLocalMap中的set/getEntry方法中 , 会对key为null(也即是ThreadLocal为null)进行判断 , 如果为null的话 , 那么是会对value置为null的 。
这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下 , 就算忘记调用remove方法 , 弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收 , 对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除 , 从而避免内存泄漏 。
5.3 hash冲突的解决hash冲突的解决是Map中的一个重要内容 。 我们以hash冲突的解决为线索 , 来研究一下ThreadLocalMap的核心源码 。


推荐阅读