ThreadLocal:线程中的全局变量 | 京东云技术团队

京东云开发者
• 阅读 470

最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。

如果全链路都变更入参和结构,很明显代码上很不优雅,后续如果还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。

到此,会想到有两种解决方案:之前用的比较少的ThreadLocal或者使用redis缓存。考虑到新增字段都是些增删改查的操作,没有必要存到redis中,故使用ThreadLocal。

一、ThreadLocal定义

以微服务架构为例,服务提供方在收到调用方的请求后,会把这个请求分配给一个线程进行处理。一般来说,一个请求会一直由同一个线程处理,中间不会切换线程,所以如果有一个线程中共享的变量,可以当全局变量使用。

ThreadLocal实现的就是一个线程中的全局变量,与真正的全局变量的区别在于ThreadLocal的变量是每个线程中的全局变量,也就是说不同线程访问到的值是不一样的。其填充的变量属于当前线程,该变量对于其他线程是隔离的。

由定义可以发现,ThreadLocal有两个特性:每个Thread的变量只能由当前Thread使用;由于其他线程不可访问,则不存在多线程间共享的问题。

二、修饰

ThreadLocal提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。

ThreadLocal变量通常被private static修饰,这样的好处是当一个线程结束时,它所使用的ThreadLocal实例副本都可被回收,避免重复创建。坏处就是这样做可能正好导致内存泄漏。

三、底层实现

ThreadLocal最朴素的内部实现是Map<threadlocal, Object>,这是一个HashMap,又称为ThreadLocalMap。但Java源码并不是Map<threadlocal, Object>的实现。这是因为如果多个线程访问同一个map,这个map需要是线程安全的,构造比较麻烦。Java采用了更简单粗暴的做法:每个线程都有自己的ThreadLocal专属map,里面可以存放多个ThreadLocal变量,这样就解决了多线程同时操作一个map带来的多线程并发问题。

因为要把ThreadLocal的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如DDD领域模型中写在Common模块。

具体的实现如下:

public class ThreadLocalUtil {

   private static ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();


   public static Integer getScene() {
       return THREAD_LOCAL.get();
   }

   public static void initScene(Integer scene) {
       if (THREAD_LOCAL == null) {
           THREAD_LOCAL = new ThreadLocal<>();
       }
       THREAD_LOCAL.set(scene);
   }

   public static void remove() {
       THREAD_LOCAL.remove();
   }
}

四、致命点

上面提到了的ThreadLocal会带来内存泄露的问题,深入分析下:

一个ThreadLocal实例对应当前线程的一个对象实例,如果把ThreadLocal声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用static修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的ThreadLocal实例引用。

ThreadLocal:线程中的全局变量 | 京东云技术团队

ThreadLocal自身并不存储值,而是作为一个key来让线程从ThreadLocal中获取value。ThreadLocalMap中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap中的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value却不为null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。

因此,key弱引用并不是导致内存泄露的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应的value

解决的方法也很简单,只需要打破引用路径中的ThreadLocalMap对对象实例的引用即可。也就是在使用完ThreadLocal之后,必须调用ThreadLocal.remove()。

延伸:

为什么要将Map中的key设置为弱引用呢?

实际上,设置key为弱引用能预防大多数内存泄露的情况。如果key使用强引用,引用的ThreadLocal对象被回收,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,也会导致内存泄露。设置为弱引用后,引用的ThreadLocal对象被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被java GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

参考文章:

https://www.cnblogs.com/tiancai/p/13141234.html?ivk_sa=1024320u

https://last2win.com/2020/09/05/java-threadlocal/

https://blog.csdn.net/u010445301/article/details/111322569

作者:京东零售 李泽阳

来源:京东云开发者社区 转载请注明来源

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
灯灯灯灯 灯灯灯灯
3年前
一次性带你了解清楚Java内存模型!
Java内存模型咳咳咳,能看完的都是人上人。。。。Java虚拟机内部使用JMM(Java内存模型)将内存划分为两个逻辑单元,线程栈(或者叫本地内存)和堆。每一个线程都有属于自己的线程栈,在线程栈中会保存局部变量(也叫做本地变量)、方法中定义的参数和异常处理器的参数(catch中的参数);这些参数和变量都属于线程局部操作,会被隔离,所以不受内存模
drools规则动态化实践
业务逻辑中经常会有一些冗长的判断,需要写特别多的ifelse,或者一些判断逻辑需要经常修改。这部分逻辑如果以java代码来实现,会面临代码规模控制不住,经常需要修改逻辑上线等多个弊端。这时候我们就需要集成规则引擎对这些判断进行线上化的管理。
Wesley13 Wesley13
3年前
Java ThreadLocal的内存泄漏问题
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:\存储单个线程上下文信息。比如存储id等;\使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;\减少参数传递。比如做一个trace工具,能够输出工程从开始到结
Wesley13 Wesley13
3年前
Vtiger CRM 几处SQL注入漏洞分析,测试工程师可借鉴
本文由云社区发表0x00前言干白盒审计有小半年了,大部分是业务上的代码,逻辑的复杂度和功能模块结构都比较简单,干久了收获也就一般,有机会接触一个成熟的产品(vtigerCRM)进行白盒审计,从审计的技术难度上来说,都比公司内的那些业务复杂得多,而真正要提高自己技术水平,更应该看的也是这些代码。vtigerCRM是一个客
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Linux应急响应(四):盖茨木马
0x00前言Linux盖茨木马是一类有着丰富历史,隐藏手法巧妙,网络攻击行为显著的DDoS木马,主要恶意特点是具备了后门程序,DDoS攻击的能力,并且会替换常用的系统文件进行伪装。木马得名于其在变量函数的命名中,大量使用Gates这个单词。分析和清除盖茨木马的过程,可以发现有很多值得去学习和借鉴的地方。0x01应急场景
Stella981 Stella981
3年前
Shell编程(逻辑判断、文件目录属性判断、if特殊用法、case判断)
shell脚本中的逻辑判断在shell脚本当中很多地方都会用到逻辑判断,判断某一个数值,判断某个目录或文件,然后针对判断的结果再做一个操作,若没有判断,是无法进行一些操作的.例如cp一些文件,或者MV,或是删除某个文件,没有判断,写这些命令就没什么意义,也就不叫作shell脚本,可见shell脚本非常重要.shell
Stella981 Stella981
3年前
Linux应急响应(二):捕捉短连接
0x00前言​短连接(shortconnnection)是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时,才去建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。在系统维护中,一般很难去察觉,需要借助网络安全设备或者抓包分析,才能够去发现。0x01应急场景​
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_