为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?

Sherwin.Wei Lv7

为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?

回答重点

使用弱引用作为 ThreadLocal 的键可以防止内存泄漏。若 ThreadLocal 实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal 实例及其对应的数据可能无法被回收,导致内存持续占用。

而弱引用允许垃圾回收器在内存不足时回收对象。这样,当没有其他强引用指向某个 ThreadLocal 实例时,它可以被及时回收,避免长时间占用内存。

扩展知识

详细分析

那为什么要这样设计呢?

假设 Entry 对 key 的引用是强引用,那么来看一下这个引用链:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353133362e706e67.png

从这条引用链可以得知,如果线程一直在,那么相关的 ThreadLocal 对象肯定会一直在,因为它一直被强引用着。

看到这里,可能有人会说那线程被回收之后就好了呀。

重点来了!线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!

所以这条引用链需要弱化一下,而能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。

与之对应的还有一个条引用链,我结合着上面的线程引用链都画出来:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353134362e706e67.png

另一条引用链就是栈上的 ThreadLocal 引用指向堆中的 ThreadLocal 对象,这个引用是强引用。

如果有这条强引用存在,那说明此时的 ThreadLocal 是有用的,此时如果发生 GC 则 ThreadLocal 对象不会被清除,因为有个强引用存在。

当随着方法的执行完毕,相应的栈帧也出栈了,此时这条强引用链就没了,如果没有别的栈有对 ThreadLocal 对象的引用,那么说明 ThreadLocal 对象无法再被访问到(定义成静态变量的另说)。

那此时 ThreadLocal 只存在与 Entry 之间的弱引用,那此时发生 GC 它就可以被清除了,因为它无法被外部使用了,那就等于没用了,是个垃圾,应该被处理来节省空间。

至此,想必你已经明白为什么 Entry 和 key 之间要设计为弱引用,就是因为平日线程的使用方式基本上都是线程池,所以线程的生命周期就很长,可能从你部署上线后一直存在,而 ThreadLocal 对象的生命周期可能没这么长。

所以为了能让已经没用 ThreadLocal 对象得以回收,所以 Entry 和 key 要设计成弱引用,不然 Entry 和 key 是强引用的话,ThreadLocal 对象就会一直在内存中存在。

但是这样设计就可能产生内存泄漏。

那什么叫内存泄漏?就是指:程序中已经无用的内存无法被释放,造成系统内存的浪费。

当 Entry 中的 key 即 ThreadLocal 对象被回收了之后,会发生 Entry 中 key 为 null 的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread->ThreadLocalMap ->Entry 这条强引用在,这样没用的内存无法被回收就是内存泄露。

那既然会有内存泄漏还这样实现?

设计者当然知道会出现这种情况,所以在多个地方都做了清理无用 Entry ,即 key 已经被回收的 Entry 的操作。

比如通过 key 查找 Entry 的时候,如果下标无法直接命中,那么就会向后遍历数组,此时遇到 key 为 null 的 Entry 就会清理掉,再贴一下这个方法:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353135352e706e67.png

这个方法也很简单,我们来看一下它的实现:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353230362e706e67.png

所以在查找 Entry 的时候,就会顺道清理无用的 Entry ,这样就能防止一部分的内存泄露啦!

还有像扩容的时候也会清理无用的 Entry:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353231392e706e67.png

其它还有,我就不贴了,反正知晓设计者是做了一些操作来回收无用的 Entry 的即可。

如果将 value 也设置为弱引用,是否可以防止内存泄漏?

答案肯定是可以的。value 一般都是局部变量赋值,栈帧出栈后,局部变量的强引用没了,如果 Entry 对其是弱引用,那么发生一次 gc 后 value 就被回收了,肯定没内存泄漏问题。

但是一次 gc 就没了,等用到的时候不就找不到 value 了?所以 value 不能被设置为弱引用

强引用、软引用、弱引用和虚引用

Comments