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)。

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值