为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
回答重点
使用弱引用作为 ThreadLocal 的键可以防止内存泄漏。若 ThreadLocal 实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal 实例及其对应的数据可能无法被回收,导致内存持续占用。
而弱引用允许垃圾回收器在内存不足时回收对象。这样,当没有其他强引用指向某个 ThreadLocal 实例时,它可以被及时回收,避免长时间占用内存。
扩展知识
详细分析
那为什么要这样设计呢?
假设 Entry 对 key 的引用是强引用,那么来看一下这个引用链:
从这条引用链可以得知,如果线程一直在,那么相关的 ThreadLocal 对象肯定会一直在,因为它一直被强引用着。
看到这里,可能有人会说那线程被回收之后就好了呀。
重点来了!线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!
所以这条引用链需要弱化一下,而能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。
与之对应的还有一个条引用链,我结合着上面的线程引用链都画出来:
另一条引用链就是栈上的 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 就会清理掉,再贴一下这个方法:
这个方法也很简单,我们来看一下它的实现:
所以在查找 Entry 的时候,就会顺道清理无用的 Entry ,这样就能防止一部分的内存泄露啦!
还有像扩容的时候也会清理无用的 Entry:
其它还有,我就不贴了,反正知晓设计者是做了一些操作来回收无用的 Entry 的即可。
如果将 value 也设置为弱引用,是否可以防止内存泄漏?
答案肯定是可以的。value 一般都是局部变量赋值,栈帧出栈后,局部变量的强引用没了,如果 Entry 对其是弱引用,那么发生一次 gc 后 value 就被回收了,肯定没内存泄漏问题。
但是一次 gc 就没了,等用到的时候不就找不到 value 了?所以 value 不能被设置为弱引用。