如何正确使用 ThreadLocal,你真的用对了吗? | 京东云技术团队
引言:
当多线程访问共享且可变的数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要 ThreadLocal 出场了。
ThreadLocal 又称线程本地变量,使用其能够将数据封闭在各自的线程中,每一个 ThreadLocal 能够存放一个线程级别的变量且它本身能够被多个线程共享使用,并且又能达到线程安全的目的,且绝对线程安全。一般用法如下:
public final static ThreadLocal<String> PARAMS = new ThreadLocal<String>();
PARAMS 代表一个能够存放 String 类型的 ThreadLocal 对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
实际上可以把企微会话存档的相关配置参数存入到 ThreadLocal 中,各个方法内需要使用直接从 ThreadLocal 中获取就可以了.
原理:我们先看一下 ThreadLocal 的结构:
首先是 set 方法:
这块代码其实很有意思,我们发现在向 ThreadLocal 中存放值时需要先从当前线程中获取 ThreadLocalMap,最后实际是要把当前 ThreadLocal 对象作为 key、要存入的值作为 value 存放到 ThreadLocalMap 中,那我们就不得不先看一下 ThreadLocalMap 的结构。
部分核心代码:
ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 内存入 的值。
ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。
每个 Thread 内部都持有一个 ThreadLoalMap 对象
我们都能够明白 ThreadLocal 存值的过程了,虽然我们是按照前言中的用法声明了一个全局常量,但是这个常量在每次设置时实际都是向当前线程的 ThreadLocalMap 内存值,从而确保了数据在不同线程之间的隔离。
接下来就是 get:
有了上面的铺垫,这段代码就不难理解了,获取 ThreadLocal 内的值时,实际上是从当前线程的 ThreadLocalMap 中以当前 ThreadLocal 对象作为 key 取出对应的值,由于值在保存时时线程隔离的,所以现在取值时只会取得当前线程中的值,所以是绝对线程安全的。
remove:
remove 将 ThreadLocal 对象关联的键值对从 Entry 中移除,正确执行 remove 方法能够避免使用 ThreadLocal 出现内存泄漏的潜在风险,int i = key.threadLocalHashCode & (len-1)这行代码很有意思,从一个集合中找到一个元素存放位置的最简单方法就是利用该元素的 hashcode 对这个集合的长度取余,如果我们能够将集合的长度限制成 2 的整数次幂就能够将取余运算转换成 hashcode 与[集合长度-1]的与运算,这样就能够提高查找效率,HashMap 中也是这样处理的。
ThreadLocal 的原理图:
在提及 ThreadLocal 使用的注意事项时,所有的文章都会指出内存泄漏这一风险,但是我发现很少有文章能够真正的把这一部分讲清楚,这里我就斗胆尝试一下,由于 ThreadLocalMap 中的 Entry 的 key 持有的是 ThreadLocal 对象的弱引用,当这个 ThreadLocal 对象当且仅当被 ThreadLocalMap 中的 Entry 引用时发生了 GC,会导致当前 ThreadLocal 对象被回收;那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 ThreadLocalMap 对象引用,ThreadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不销毁的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。
解决办法:
我们知道出现内存泄漏的原因是失去了对 ThreadLocal 对象的强引用,避免内存泄漏最简单的方法就是始终保持对 ThreadLocal 对象的强引用,为每个线程声明一个对 ThreadLocal 对象的强引用显然是不合适的(太麻烦且缺乏声明的时机),所以,我们可以将 ThreadLocal 对象声明为一个全局常量,所有的线程均使用这一常量即可,例如:
按照上面的方式声明 ThreadLocal 对象后,所有的线程共用此对象,在使用此对象存值时会把此对象作为 key 然后把对应的值作为 value 存入到当前线程的 ThreadLocalMap 中,由于此对象始终存在着一个全局的强引用,所以其不会被垃圾回收,调用 remove 方法后就能够将此对象关联的 Entry 清除。
结果如下:
可以看出两个线程内对应的 Entry 的 key 为同一个对象且即使发生了垃圾回收该对象也不会被回收。
那么是不是说将 ThreadLocal 对象声明为一个全局常量后使用就没有问题了呢,当然不是,我们需要确保在每次使用完 ThreadLocal 对象后确保要执行一下该对象的 remove 方法(重要),清除当前线程保存的信息,这样当此线程再被利用时不会取到错误的信息(使用线程池极易出现);
常见的使用场景:
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
线程间数据隔离,各线程的 ThreadLocal 互不影响
方便同一个线程使用某一对象,避免不必要的参数传递
全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
Spring 事务管理器采用了 ThreadLocal
Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
一个 APP 多个数据源,来回切换多个数据源进行查询数据。
日期格式化实例多线程安全问题。
总结:
本文主要从源码的角度解析了 ThreadLocal,并分析了发生内存泄漏的原因及正确用法,最后对它的应用场景进行了简单介绍。
ThreadLocal 还有其他变种例如 FastThreadLocal 和 TransmittableThreadLocal,FastThreadLocal 主要解决了伪共享的问题比 ThreadLocal 拥有更好的性能,TransmittableThreadLocal 主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题等。
作者:京东零售 郭春元
来源:京东云开发者社区
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/8ce9038269b682b5050d67d28】。文章转载请联系作者。
评论