ThreadLocal浅析

ThreadLocal在Java中用于保存线程局部变量,提供线程安全的上下文信息访问,避免同步带来的性能开销。每个线程拥有独立的ThreadLocalMap,避免数据共享。ThreadLocalMap使用弱引用存储,防止内存泄漏,但在线程持续运行且未调用remove时,可能导致内存泄漏。文章详细解析了ThreadLocal的结构、源码及内存管理策略。

ThreadLocal的用途

1、保存线程的上下文信息,在任意需要的地方可以获取,但不被多线程共享

由ThreadLocal的特性可知,在同个线程里面,针对同个ThreadLocal对象的赋值,在线程中都是可以根据该ThreadLocal对象获取的,而ThreadLocal同时也是线程私有的,不用担心共享数据的问题。

对服务器端来说,每次请求都是一条线程处理,可以把请求的数据放在ThreadLocal中,从controller --> service --> dao层,甚至RPC调用,都不用修改方法参数或类变量,直接通过ThreadLocal设置或获取即可

2、线程安全的,避免某些需要考虑线程安全必须同步带来的性能损失

ThreadLocal其实无法处理共享数据的多线程并发问题的,它最多只是把共享数据在自己的线程内部拷贝了一个副本而已,针对共享数据的写操作还是会有并发问题的,线程的共享数据的拷贝也无法知道共享数据的最新的值
在这里插入图片描述

ThreadLocal结构图

在这里插入图片描述

踩坑:

之前认为ThreadLocalMap是所有线程公用这个集合的,后来发现是每个线程都维护了一个map集合,即threadLocals字段,key为不同的ThreadLocal对象

源码部分

1、常用方法介绍

在这里插入图片描述

2、hash冲突解决

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
在这里插入图片描述
关于0x61c88647这个数字是如何让哈希码能均匀的分布在2的N次方的数组里,详情可看:https://zhuanlan.zhihu.com/p/40515974

3、常用方法分析

名词定义

后续的说明都直接使用名词代码

条目、槽:table表中的Entry元素

(陈旧)空槽、陈旧条目:Entry对象为空,可以被下次hash使用的空槽

脏数据:Entry对象不为空,key(ThreadLocal对象)为空的表数据

get

在这里插入图片描述

getEntry

在当前线程中的ThreadLocalMap中获取指定ThreadLocal关联的value
在这里插入图片描述

expungeStaleEntry

清理脏数据

从staleSlot开始,清除key为null的Entry,并将不为空的元素放到合适的位置,最后遍历到Entry为空的元素时,跳出循环返回当前空槽索引位置
当前位置 -> 第一个空槽位置 之间的脏数据进行清理,此时空槽位置之后的脏数据并没有清理
在这里插入图片描述

set

在这里插入图片描述

replaceStaleEntry

在这里插入图片描述

cleanSomeSlots

在这里插入图片描述

rehash

在这里插入图片描述

remove

在这里插入图片描述

内存泄漏

ThreadLocal对象在调用set方法的时候会把自己作为Entry的key,而Entry的key是作为弱引用存在的,至于什么是弱引用,可以参考下图:

在这里插入图片描述

threadLocal,threadLocalMap,entry之间的关系如下图所示:

在这里插入图片描述

(引用)上图中,实线代表强引用,虚线代表的是弱引用,如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,很显然在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。当然,如果线程执行结束后,threadLocal,threadRef会断掉,因此threadLocal,threadLocalMap,entry都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们关注。

实际上,为了解决threadLocal潜在的内存泄漏的问题,jdk对其有一些措施

set、get、remove方法,在遍历的时候如果遇到key为null的情况,都会调用expungeStaleEntry方法来清除key为null的Entry,等下一次垃圾回收时,这些Entry将会被彻底回收。

但是如果当前线程一直在运行,并且一直不执行get、set、remove方法,这些key为null的Entry的value就会一直存在一条强引用链:Thread
Ref -> Thread -> ThreadLocalMap -> Entry -> value,导致这些key为null的Entry的value永远无法回收,造成内存泄漏。

为了避免这种情况,我们可以在使用完ThreadLocal后,手动调用remove方法,以避免出现内存泄漏。

当使用线程池时还需要考虑上一次使用造成的旧数据问题,因为线程是同一个,所以可能导致取值拿到上次的旧数据,使用完调用remove方法完成数据的清理对于线程复用是个很好的方法。

为什么使用弱引用
  • 假设threadLocal使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉threadLocal实例的目的,但是因为threadLocalMap的Entry强引用threadLocal,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误。并且threadLocal的生命周期里(set,getEntry,remove)里,不会对脏Entry进行清理。
  • 假设Entry弱引用threadLocal,尽管会出现内存泄漏的问题,但是在threadLocal的生命周期里(set,getEntry,remove)里,都会针对key为null的脏entry进行处理。
    从以上的分析可以看出,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。

Thread.exit()

当线程退出时会执行exit方法,从源码可以看出当线程结束时,会令threadLocals=null,也就意味着GC的时候就可以将threadLocalMap进行垃圾回收,换句话说threadLocalMap生命周期实际上thread的生命周期相同。
在这里插入图片描述

总结

  1. 对于一次性使用的线程,其实是不存在内存泄漏的问题的,线程执行结束,threadLocalMap会被回收掉,当前threadLocalMap的Entry对象都会被回收。
  2. 当ThreadLocal实例为空,就会被gc回收,导致Entry中key为null的脏数据,如果线程一直运行,并且不进行任何操作的话,Entry的value永远不会回收,导致内存泄漏。(使用remove清理)
  3. 使用线程池,需要考虑上一次使用造成的旧数据问题,因为线程是同一个,所以可能导致取值拿到上次的旧数据,然后如果其中的线程后续没有对该ThreadLocal进行任何操作,value也会常驻于内存中,和第二点的情况一样,所以使用完进行remove操作是很有必要的。

参考和引用

  • https://blog.youkuaiyun.com/tujisitan/article/details/106176680
  • https://blog.youkuaiyun.com/u010004317/article/details/105249923
  • https://zhuanlan.zhihu.com/p/34406557
  • https://cloud.tencent.com/developer/article/1333298
  • https://www.cnblogs.com/kaleidoscope/p/9588151.html
  • https://zhuanlan.zhihu.com/p/40515974
  • https://www.sohu.com/a/349724415_99908665
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值