ThreadLocal的使用及内存泄漏问题源码解析

ThreadLocal的作用

ThreadLocal用于线程内数据共享,或者说线程间数据隔离。它可以实现在多线程中为每一个线程提供各自的变量副本。简单来说,使用ThreadLocal.set(obj)方法保存的数据,只能在同一线程中调用ThreadLocal.get()才能取出。

ThreadLocal如何实现线程间隔离?

首先看看ThreadLocal.set()方法

 

 

将当前线程取出,获取线程中的成员变量ThreadLocalMap,将需要保存的值设置到此map中。所以每条线程中都有自己的ThreadLocalMap,可用于保存当前线程的数据。

继续看看这个ThreadLocalMap

 

可以看到,ThreadLocalMap是ThreadLocal的一个内部类,在其内部还定义了一个Entry类,这个Entry的key使用弱引用指向一个ThreadLocal对象,其value中保存的就是我们通过ThreadLocal.set(value)方法设置的value值。可以理解为Entry是一个键值对,key为ThreadLocal对象,value为对应的值。通过红框里的代码可以看到,ThreadLocalMap中维护的是一个Entry数组,所以每条线程可以通过多个ThreadLocal对象保存多个value值。下图可以帮助理解ThreadLocal的引用关系。

 

为什么ThreadLocalMap中的Entry会采用弱引用?

弱引用的使用场景很简单,就是为了解决内存泄漏问题,这里也是一样。那为什么使用强引用ThreadLocal会发生内存泄漏呢?在多线程的程序中,线程的创建和销毁都是开销比较大的操作,一般都会使用到线程池,线程池中的线程使用完成后会放回池中而不是直接销毁。这就造成线程的ThreadLocalMap会一直持有Entry对象的引用,threadLocal和我们保存的value对象也自然无法被gc回收,造成内存泄漏。最好的解决办法当然是使用完这个value后主动调用ThreadLocal的remove方法将其移除,但如果我们没有做这个操作,线程被复用后又继续往当前线程的ThreadLocalMap中设值,就会进一步加大内存溢出的风险。而如果entry和threadLocal之间是弱引用,threadLocal对象在其所在的方法执行完成后,就只剩下entry一条引用链,下一次gc发生时,threadLocal对象就能顺利被垃圾回收。但还有一个问题,threadLocal被回收后,以它为key的这个value值就无法再被访问,此时,这个entry实例就成为了一个脏数据占用在当前线程的ThreadLocalMap中并且无法被gc。这个问题又是如何解决(优化)的呢?答案在ThreadLocal的Set方法中。

ThreadLocal是如何解决内存泄漏问题的?

回到ThreadLocal的set方法

 

进入map.set(this,value)

 

 

看红框1中的代码,这里是用当前ThreadLocal对象实例的hash值算出它存放在entry数组中的index坐标。如下图所示,每个threadLocal对象实例的hash值(threadLocalHashCode)都不相同,内存中第一次创建的threadLocal实例hash值为HASH_INCREMENT,之后每一次创建实例的hash值都为前一个实例的hash值加上固定值HASH_INCREMENT。这个固定值是0x61c88647,等于2^32*黄金分割比,为什么用这个数这里不过多展开,总的来说,这里使用了斐波那契散列算法,threadLocalhashCode & (len-1)的值会均匀的分布在长度为2^N的数组中(ThreadLocalMap中entry数组的初始化容量为16,即2^4,每次扩容增加1倍),最大限度的减少hash冲突。回到正题,threadLocal对象被回收后,entry在什么情况下会被销毁?分为以下几种情况

1.在set方法设值中,计算坐标i时发生了hash冲突,取出数组中这个i坐标下的entry,发现key为null,则进入红框2中的方法,replaceStaleEntry()翻译过来就是替换过期entry的意思,会将新的threadLocal对象和value值设置到这个key已被回收的entry中。

2.计算坐标i时没有发生hash冲突,则初始化一个entry放入数组中,进入红框3中代码:取出数组中当前坐标i的下一个坐标中的entry,判断其key是否为null,如果是则将其移除,然后循环判断

 

3.计算坐标i时没有发生hash冲突,判断i之后坐标的entry对象也没有发现脏数据,并且entry数组中存放的entry数量已经大于或等于threshold(数组长度的三分之二),则进入rehash()方法:遍历清除所有key已被回收的entry,清除完成后数组中entry的数量依然大于threshold - threshold / 4则将数组扩容一倍(初始化时数组长度为2^4)。

 

 

 

 

 

 

ThreadLocal中,内存泄露问题通常是由于没有正确地清理ThreadLocal对象造成的。当一个线程调用ThreadLocal的set方法设置变量时,当前线程ThreadLocalMap里面就会存放一个Entry对象,这个记录的key为ThreadLocal的引用,value则为设置的值。如果当前线程一直存在而没有调用ThreadLocal的remove方法,并且这时候其它地方还是有对ThreadLocal的引用,则当前线程ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。即使ThreadLocal变量没有了其他强依赖,而当前线程存在,由于线程ThreadLocalMap里面的key是弱依赖,那么在垃圾回收时,当前线程ThreadLocalMap里面的ThreadLocal变量的弱引用会被回收,但是对应的value还是会造成内存泄露。因此,在使用ThreadLocal时,需要确保及时调用remove方法来清理ThreadLocal引用,以避免内存泄露的问题。另外,使用线程池的情况下,使用ThreadLocal一定要使用remove方法即时清理,因为ThreadLocal是属于某个线程的,而在使用线程池的情况下,这些线程都是可重复利用、存活时间长的线程,如果在使用过程中不及时remove,就会造成内存泄露的问题,并且可能引发一些功能逻辑问题,比如多个请求可能获取到了线程池中同一个线程ThreadLocal值。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [ThreadLocal的内存泄露问题](https://blog.csdn.net/sunao1106/article/details/127133911)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [记一次ThreadLocal引发的内存泄露](https://blog.csdn.net/kshzhaohui/article/details/111244981)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值