个人谈谈对ThreadLocal内存泄露的理解
- ThreadLocal作用
- ThreadLocalMap内存泄露解释
- 为什么要这样设计
- ThreadLocalMap的实现思路
ThreadLocal作用
平时我们会使用ThreadLocal来存放当前线程的副本数据,让当前线程执行流中各个位置,都可以从ThreadLocal中获取到想要的线程副本数据,而无需通过方法参数逐级传递,减少了代码的耦合。
那么我们通过ThreadLocal设置的线程副本数据具体是保存在哪里的呢? 怎么保存的呢?
这里简单说一下: 我们其实是通过ThreadLocal对象间接操作Thread对象内部的ThreadLocalMap线程副本数据存储源的
首先,基于OOP思想,Thread类应该聚合了当前线程相关信息,如: 线程ID,线程名,线程副本数据存储源等 。
为什么不直接通过Thread对象暴露出接口来访问内部的ThreadLocalMap,而采用ThreadLocal进行间接访问,这其实是遵循了"最小知道原则",即: 如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
因为我们只是想设置和保存数据到当前线程的存储源中,而不想知道线程对象其他细节,因此采用ThreadLocal实现这一特定功能。
扩展一点: 之所以ThreadLocal对象单独设计成一个类,而不是以静态内部类的形式出现在Thread类中,是因为这遵循了"单一职责原则",线程副本数据并不是线程对象必须具备的属性,类设计的时候只保留本身必须的属性即可。
ThreadLocalMap内存泄露解释
ThreadLocalMap本身是由一组Entry组成的,每个Entry具体又包含了key和value两部分,key的类型是ThreadLocal,val就是我们设置到线程的副本数据。
此处Entry的key采用的是弱引用实现的:
实际我们传入的ThreadLocal对象是被WeakReference弱引用类中的referent属性指向的,表示当前ThreadLocal被一个弱引用对象指向着:
内存泄露发生场景:
由于key为null,value依然占据内存空间,但是无法被访问到,所以就称这种情况下产生了内存泄露。
为什么要这样设计
为什么要把ThreadLocalMap中的Entry设置成弱引用对象呢?如果设置成普通的map集合会怎么样呢?
首先,我们采用普通的map集合作为线程副本数据存储实现,那么当前我们的应用程序失去了对ThreadLocal对象的强引用时,我们就再也无法通过ThreadLocal去访问ThreadLocalMap中我们存储的线程副本数据了,那么此时就可以认为这样一对key:value键值对是垃圾,需要被回收掉。
对于普通的map实现而言,我们无法区分到底哪些ThreadLocal对象确定是应用程序不再访问的,可以被回收掉的,因此也就无法回收这些垃圾键值对占据的空间了,反而会导致某种意义上的内存泄露。
关键问题就是如何知道哪些ThreadLocal对象不会再被应用程序访问,也就是说哪些ThreadLocal对象不再被应用程序中某些变量强引用指向,这个解决办法就是将map中的key设置为弱引用类型。
当我们将map中的key设置为弱引用类型时,当应用程序不再通过强引用指向某个ThreadLocal对象时,我们便可以通过垃圾回收器感知到这一情况,因为垃圾回收器会在垃圾回收时,回收掉这些只被弱引用对象指向的ThreadLocal对象,回收后,对于key就被设置为了NULL,此时Entry不为null。
我们可以对这些key为null的键值对进行清理回收,然后重用这些空间。
ThreadLocalMap的实现思路
此处参考下面这篇文章,来简单聊聊ThreadLocalMap的一个设计思路:
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
set过程:
- 计算ThreadLocal对象的hashcode,然后取余数组大小,得出最终需要放置的数组索引位置
- 如果产生hash冲突,采用线性探测法解决,不冲突判断entry是否为null,或者entry的key是否为null ,满足其一,说明该空间可以被使用。不满足,判断key是否相等,相等则进行覆盖操作。
- 进入过期key清理过程:
- 首先第一步计算得出数组索引位置处开始,向前寻找到过期key首次出现的位置
- 从首次出现的位置开始往后执行探测式清理工作,清理过程为:
- 遍历到过期entry则设置entry为null
- 碰到未过期的entry,通过rehash进行位置重定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的entry为null的桶中,使得rehash后的entry数据距离正确的位置更近一些,减少get时的遍历损耗。
- 当遇到entry为null的情况时,结束探测式清理工作。
get过程:
- 计算ThreadLocal对象的hashcode,然后取余数组大小,得出数据存放在数组索引位置
- 该位置的entry的key与查找的key一致,直接返回
- 不一致则采用线性探测法往后遍历,判断哪一个entry的key和当前要查看的key是一致的
1.这个探测过程中,如果发现了某个entry.key为null,则会进行一次探测式垃圾回收,回收完后,继续往后遍历