如何正确使用 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的结构。
部分核心代码:
static class ThreadLocalMap { // 键值对实体的存储结构 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; // k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用,v 作 value Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * 初始容量,必须为 2 的幂. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0 }
ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,从源码中看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal内存入 的值。
ThreadLocalMap 解决 hash 冲突的方式采用的是「线性探测法」,如果发生冲突会继续寻找下一个空的位置。
每个Thread内部都持有一个ThreadLoalMap对象
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
我们都能够明白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主要解决了线程池中线程复用导致后续提交的任务并不会继承到父线程的线程变量的问题等。
作者:京东零售 郭春元
来源:京东云开发者社区

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
突破传统监测模式:业务状态监控HM的新思路 | 京东云技术团队
一、传统监控系统的盲区,如何打造业务状态监控。 在系统架构设计中非常重要的一环是要做数据监控和数据最终一致性,关于一致性的补偿,已经由算法部的大佬总结过就不再赘述。这里主要讲如何去补偿?补偿的方案哪些?这就引出来数据监控系统了。有小伙伴会问了,为什么业务状态监控系统可以做补偿?别急,往下看。 传统监控系统分为两种,系统监控和业务监控。系统监控有并发量监控、异常监控、调用链监控、端口监控、zabbix 监控、http监控等。业务监控是指用以监控业务数据是否正常,用户需要进行业务埋点进行数据采集。业务监控底层常规依赖日志上报系统,接入业务监控之前先申请接入日志上报系统。如图1 (图1) 从业务监控时序图中看到一般分为五步: 1. 数据埋点,业务端埋点后上报的日志,也可以是mysql。日志文件最后通过flume或者bin log上报。 2. 数据收集,通常都通过kafka做数据采集。 3. 数据清洗,一般都是在ods层用spark-streaming进行分流,清洗。 4. 数据存储,数据分流后会存储到dw层,最后落到各种库里面。 5. 数据展示,开源的很多,用的多还是grafana,还...
- 下一篇
云智慧x统信软件:智能化IT服务管理,提升客户服务价值
统信软件由中国领先的操作系统厂商于2019年联合成立,是全球主流的操作系统产品及服务提供商,致力于研发安全稳定、智能易用的操作系统产品,拥有统信UOS桌面版、服务器版、智能终端版在内的全栈基础设施,以及集中域管平台、企业级应用商店、平台迁移软件等自研产品矩阵。根据第三方权威数据,统信UOS桌面版市占率持续位居第一,服务器版增速行业第一。 随着数字化转型步伐的加快,越来越多的企业发现IT服务已成为获得竞争优势和管理效率的的重要推动者。统信软件非常重视客户服务质量及IT运营能力的体系建设,云智慧ITSM的正式上线运营标志着统信软件IT服务管理标准化、自动化、智能化能力的全面提升: 避免信息孤岛问题 信息孤岛会带来数据不一致、读写冲突、流程回退等一系列问题,ITSM作为重要的数据、流程通道,可以打通CRM和产研后台,实现CRM、ITSM、产研后台三位一体的IT解决方案,提供更加及时有效的业务持续性服务。 更容易通过root cause来解决共性问题 技术服务不会像平静的水面,而是会时时泛起涟漪。作为技术服务团队,每天都会收到并处理大量的客户问题。这些问题中,总会有一些是共性问题。ITSM可以...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19