再谈谈ThreadLocal

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

大部分面试官喜欢问ThreadLocal,却错误地以为东西是存在ThreadLocal中,并且笃定key是当前线程...

其实Java的线程共享机制,最重要的是Thread中的ThreadLocalMap,ThreadLocal其实不重要,它只是一个钩子。东西实际被存在每一个Thread的ThreadLocalMap中,所以广义上可以理解为东西是存在Thread中。关于三者的关系,急性子的朋友可以直接先拉到文章末尾看看那张图。

ThreadLocal.set(T value)的key是Thread吗?

首先,要更新一下大家固有的认知:ThreadLocal其实不存东西,ThreadLocalMap的key也不是Thread。

很多人,包括很多面试官,都认为ThreadLocal在执行set(T value)时是把当前线程作为key存入自己内部的map中,大致相当于这样:

为什么他们会这样认为呢?大概是因为他们“看过”threadLocal.set(T value)的源码:

ThreadLocal tl1 = new ThreadLocal();
t1.set("你好");

createMap(t, value),我去,这不就是传入一个Thread和value然后在内部构建一个Map吗?不用想了,ThreadLocal内部肯定有个Map,key就是Thread!

但真的是这样吗?

NO,我们先不管map是哪来的,直接看if(map != null)的逻辑:map.set(this, value),也就是把this作为map的key。

那么,这个this是谁呢?很明显,是ThreadLocal。

现在我们隐约了解到变量存在某个Map中,而且key是ThreadLocal。那么这个Map是谁?怎么来的?

答案就在createMap(t, value):t就是当前线程,由Thread.currentThread()得到。

哦!原来Map就是这么来的:通过createMap()方法new了一个ThreadLocalMap,并且做了初始化,把当前threadLocal对象作为key,firstValue作为value。

至此,我们解决了两个疑问:

  • 有一个不知道来历的Map,变量都存在它那,key是ThreadLocal,而不是大部分人认为的Thread
  • Map的类型是ThreadLocalMap,被赋值给Thread的一个字段:threadLocals

是不是有点懵?没关系,上面这段话就是来打破固有认知的,看不懂也没关系,我当初也绕了很久。

下面开始正文。

如何理解ThreadLocal是一个钩子?

如上图,Thread t1被实例化后,其实内部有个threadLocals字段,它其实是一个ThreadLocalMap,刚开始是null。然后t1.start()后线程就开始跑了(沿着箭头),当线程执行到

ThreadLocal tl1 = new ThreadLocal();
t1.set("你好");

时,ThreadLocal会作为一个钩子,尝试从Thread t1中钩出ThreadLocalMap。如果发现这个成员变量尚未赋值,则new ThreadLocalMap()并把map设置进去。特别注意,由于set()是ThreadLocal的方法,所以map.set(this, value)中的this显然是ThreadLocal tl1。

所以,ThreadLocalMap的key并不是Thread,而是ThreadLocal!!!

此时此刻,内存中有三个对象,Thread t1、ThreadLocal tl1、ThreadLocalMap map,其中ThreadLocalMap在Thread内部,是ThreadLocal塞进去的。

你可以理解为:

ThreadLocal是紫霞仙子,而Thread是至尊宝,500年前在花果山的时候,紫霞一剑劈开至尊宝的胸膛(getMap),看看他有没有心(ThreadLocalMap)。此时发现至尊宝没有心,于是造了一颗心并且在心里留下一滴眼泪(紫霞:泪水)

再看一遍上面的流程图,看看是不是这么回事?

调用threadLocal.get()到底发生了什么?

500年后,至尊宝走啊走,走到了盘丝洞,又遇到了紫霞仙子(ThreadLocal),紫霞再次劈开了至尊宝的胸膛(getMap),发现已经有心了,于是在至尊宝的心里找到名为“紫霞”的那滴泪水。

至此,大家已经明白同一个thread是如何在Controller存入值,然后在Service取出值的。

多个Thread与同一个ThreadLocal

上面讲的是一个Thread和一个ThreadLocal。接下来,我们探究一下多个Thread与同一个ThreadLocal:为什么访问同一个threadLocal.get(),Thread1存入的值不会被Thread2取出来?

其实很简单,你想想,同一个ThreadLocal表示从始至终只有一个紫霞仙子,而Thread1和Thread2可以看做是至尊宝和孙悟空。

至尊宝见到紫霞--->threadLocal.set(),紫霞劈开至尊宝的胸膛,造了一颗心并留下泪水(紫霞:泪水)--->紫霞把心(map)塞回至尊宝胸膛

孙悟空见到紫霞--->threadLocal.set(),紫霞劈开孙悟空的胸膛,造了一颗心并留下紫青宝剑(紫霞:紫青宝剑)--->紫霞把心(map)塞回孙悟空胸膛

至尊宝又见到紫霞,紫霞拿出至尊宝的心,取出泪水。

孙悟空又见到紫霞,紫霞拿出孙悟空的心,取出紫青宝剑。

也就说,至尊宝和孙悟空各自都在路上遇到了紫霞,心里都存了东西。

至尊宝的心

{

"紫霞":"泪水"

}

孙悟空的心

{

"紫霞":"紫青宝剑"

}

ThreadLocal zixia = new ThreadLocal();// 至尊宝
new Thread(()->{// 遇到紫霞,紫霞往他心里塞了眼泪zixia.set("泪水");
}).start();// 孙悟空
new Thread(()->{// 遇到紫霞,紫霞往他心里塞了紫青宝剑zixia.set("紫青宝剑");
}).start();

至尊宝和孙悟空不是同一个人啊,紫霞分别在他们心里放的东西,怎么会串起来呢?

同一个Thread与多个ThreadLocal

至尊宝见到紫霞--->threadLocal.set(),紫霞劈开至尊宝的胸膛,造了一颗心并留下泪水(紫霞:泪水)--->紫霞把心(map)塞回至尊宝胸膛。

至尊宝(Thread)和紫霞(ThreadLocal)快乐地生活着,此时他的心里(ThreadLocalMap)有一颗紫霞的泪水。但有一天他在菜市场遇到了初恋白晶晶(另一个ThreadLocal),白晶晶劈开至尊宝的胸膛,发现已经有心了(ThreadLocalMap),就不造心了,而是在里面留下一段回忆(白晶晶:回忆)

此时至尊宝的心是这样的:

{

"紫霞":"泪水",

"白晶晶":"回忆"

}

也就是说,一个Thread只能有一个ThreadLocalMap,第一次遇到的ThreadLocal会帮它创建一个Map塞进去,往后无论遇到多少个ThreadLocal,都是直接用那个Map,而且都是把自己作为key,往Map里存东西。

ThreadLocal zixia = new ThreadLocal();
zixia.set("泪水");ThreadLocal baijingjing = new ThreadLocal();
baijingjing.set("回忆");

线程你是看不到的,但可以想象一阵风吹过上面两个ThreadLocal对象,每个ThreadLocal对象都尝试把变量塞到风中,往下一个站点带去。

多个Thread与多个ThreadLocal

如果你顺着箭头看,会发现thread-0只能访问threadLocalMap@111,thread-1只能访问threadLoocalMap@222,因为ThreadLocalMap本质是每个Thread内部各存一份,互不干扰。Thread在遇到不同的ThreadLocal,可以把ThreadLocal自身作为key存入map或从map中取出value。

ThreadLocalMap与WeakReference

在Java中有4种引用类型:强、软、弱、虚。

  • 强引用不受GC影响,除非引用全部切断。比如 Student s = new Student(),假设当前只有s指向Student对象,那么当s=null时,Student对象会在下次GC时被回收
  • 软引用对象会在内存不足触发GC时被回收(适用于高速缓存)
  • 弱引用是每次GC时都回收,不论内存是否不足
  • 虚引用(堆外内存,比如zerocopy)

对于Map,每一个键值对被称为Entry,相信大家都知道。

为什么ThreadLocalMap的Entry要继承弱引用呢?

在解释这个问题之前,我们先来了解弱引用的概念以及它的一般用法:

也就是说,当一个对象被WeakReference包装后,它就产生了一个弱引用指向它。此时即使把强引用切断,仍然有弱引用连接着。但是由于弱引用的特性,这个对象会在下次被GC线程被直接回收。

好了,让我们再次回到ThreadLocalMap。虽然Entry继承了WeakReference,但并不是说Entry本身是弱引用,而是Entry的key是弱引用:

于是问题由为什么ThreadLocalMap的Entry要继承弱引用转为ThreadLocalMap为什么要把key包装成弱引用

如果ThreadLocalMap的key不使用Weak Reference,那么堆中的ThreadLocal对象同时存在多处强引用,即使我们把外面的threadLocal设置为null,但ThreadLocalMap中的引用仍然指向堆中的ThreadLocal。最终可能造成内存泄露(无法彻底释放ThreadLocal对象,因为始终有引用指向它)。

而如果key是弱引用,一旦在某一刻,外界所有强引用都被切断(外面的ThreadLocal被置为null),当前只有弱引用指向ThreadLocal对象,那么不久的将来(下一次GC)ThreadLocal对象就会被回收。

ThreadLocalMap与内存泄露

以为我讲重复了吗?上面不是已经用弱引用解决了吗?!

并没有。

原本引入Weak Reference是为了解决多个强引用导致ThreadLocal对象无法回收的问题,但一个解决策略的引入往往伴随着新bug的产生。试想一下,当外部强引用都切断后下一次GC回收了ThreadLocal对象,此时Entry的key会变成什么?

key = null;

当tl1变成null,ThreadLocalMap的Entrys变成下面这样:

  • null : value1(Entry1)
  • tl2 : value2(Entry2)
  • tl3 : value3(Entry3)

从此以后Entry1再也没有人能回收了,因为tl1已经被回收,这个key没了,自然也就无法根据key清除value了。C/C++中有“野指针”的概念,所以我喜欢把这种情况称为“野Entry”。

在引入弱引用前,我们担心的是ThreadLocal一直无法被释放造成内存泄漏,而引入了WeakReference后虽然解决了ThreadLocal的内存泄露,却可能导致Entry的内存泄露,因为当key变成null后,我们无法再根据key移除value了。

实际上,ThreadLocalMap也发现了这个问题,它会在每次get/set时判断key,如果key为null,则把value也归置为null:

但是这种策略是不保险的,因为它的前提是下一次使用时把上一次遗留的key为null的value清除。如果我再也不用,是不是仍然无法移除呢?

所以最保险的方法是,每次使用完毕都及时清除。

ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("紫霞");
// ...经历过好多事
tl.remove()

remove()是ThreadLocal的方法,this指的是threadLocal,就是从Map中根据key删除value。

  • tl1 : value1 (根据key把value清空)
  • tl2 : value2
  • tl3 : value3

《阿里巴巴开发手册》也是这样建议:

一般来说,用完立即移除是最好的,但实际编程时有些公司喜欢在拦截器中取出用户信息放入线程,对此个人建议可以在拦截器的preHandle()中set,在afterCompletion()中remove()。

最后,一张图总结Thread、ThreadLocal和ThreadLocalMap:

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/201175.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

金蝶云星空表单插件获取基础资料的内码

文章目录 金蝶云星空表单插件获取基础资料的内码 金蝶云星空表单插件获取基础资料的内码 不能直接取内码 先获取基础资料数据包&#xff0c;再获取内码 long custId Convert.ToInt64((this.View.Model.GetValue("F_XXXX_CustId") as DynamicObject)["Id&quo…

【从零开始学习JVM | 第三篇】类的生命周期(高频面试)

前言&#xff1a; 在Java编程中&#xff0c;类的生命周期是指类从被加载到内存中开始&#xff0c;到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。 在本文中&#xff0c;我们将深入探讨类的生命周期&#xff0c;从类加载到…

ros2与stm32通讯比较优秀的串口库

这个是我确定的串口库&#xff1a;serial: serial::Serial Class Reference (wjwwood.io) 我也不知道其他的串口库了&#xff0c;我就知道几个&#xff0c;然后我觉得这个是3个里面学习周期比较短&#xff0c;然后质量比较可靠的库 我隐隐觉得这个串口库就是ros1选择的串口库…

shell命令使用杂七杂八(1)——(待完善)

explainshell.com shell统计当前文件夹下的文件个数、目录个数Linux之shell常用命令&#xff08;三&#xff09; sort&#xff08;排序&#xff09;、uniq&#xff08;处理重复字符&#xff09; linux中shell将换行输入到文件中 shell脚本&#xff0c;将多行内容写入文件中 f…

PySpark开发环境搭建常见问题及解决

PySpark环境搭建常见问题及解决 1、winutils.exe问题2、SparkURL问题3、set_ugi()问题 本文主要收录PySpark开发环境搭建时常见的一些问题及解决方案&#xff0c;并收集一些相关资源 1、winutils.exe问题 报错摘要&#xff1a; WARN Shell: Did not find winutils.exe: {} ja…

批量创建/更新外协工序采购信息记录

批量创建/更新没有物料号的外协工序采购信息记录。 执行事务代码ZME1X_OP,下载模板。(此程序可同时用于外协工序的创建和修改)创建外协工序的时候如果是新建则不需要输入采购信息记录号,如果是要更新外协工序价格,则必须输入采购信息记录号。价格单位默认为‘1’,货币代码…

IPV6技术

配置广域网接入&#xff1a; PPP协议&#xff1a;点对点协议是作为点对点链路上进行IP特性的封装协议而被开发出来的。 ppp定义了IP地址的分配和管理、异步和面向位的同步封装、网络协议复用、链路配置、链路质量测试和错误检测等标准&#xff0c;以及网络层地址协议和数据压缩…

智能井盖倾斜预防方案,井盖监测方式推荐

随着每个城市的日益发展&#xff0c;城市基础设施的重要性慢慢凸显出来。其中井盖作为城市生命线的一部分&#xff0c;其安全问题不容小觑。为了保障市民的出行安全和城市基础设施正常运行&#xff0c;智能井盖传感器作为城市生命线智能监测仪有着重要的作用&#xff0c;该设备…

深圳锐杰金融的慈善承诺:健康社区,绿色未来

深圳市锐杰金融投资有限公司&#xff0c;作为中国经济特区的中流砥柱&#xff0c;近年来以其杰出的金融成绩和坚定的社会责任立场引人注目。然而&#xff0c;这并非一个寻常的金融机构。锐杰金融正在用自己的方式诠释企业责任和慈善精神&#xff0c;通过一系列独特的慈善项目&a…

封装ThreadLocal

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 为什么要封装ThreadLoc…

玄子Share-CSS3 弹性布局知识手册

玄子Share-CSS3 弹性布局知识手册 Flexbox Layout&#xff08;弹性盒布局&#xff09;是一种在 CSS 中用于设计复杂布局结构的模型。它提供了更加高效、简便的方式来对容器内的子元素进行排列、对齐和分布 主轴和交叉轴 使用弹性布局&#xff0c;最重要的一个概念就是主轴与…

CoDeF视频处理——视频风格转化部署使用与源码解析

一、算法简介与功能 CoDef是作为一种新型的视频表示形式&#xff0c;它包括一个规范内容场&#xff0c;聚合整个视频中的静态内容&#xff0c;以及一个时间变形场&#xff0c;记录了从规范图像&#xff08;即从规范内容场渲染而成&#xff09;到每个单独帧的变换过程。针对目标…

JavaScript中的this指向:如何避免常见的this陷阱

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;JavaScript篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript-this指向 目录 this指向详解 强行改变 this 指向 修改上下文中的this…

17、pytest自动使用fixture

官方实例 # content of test_autouse_fixture.py import pytestpytest.fixture def first_entry():return "a"pytest.fixture def order():return []pytest.fixture(autouseTrue) def append_first(order, first_entry):return order.append(first_entry)def test_s…

数学建模-基于机器学习的家政行业整体素质提升因素分析

基于机器学习的家政行业整体素质提升因素分析 整体求解过程概述(摘要) 家政服务业即为家庭提供多种类服务的专门行业&#xff0c;在第三产业中占有重要地位。但近年来&#xff0c;由于人工智能家居产业的发展与客户对家政从业者的要求水平不断提高&#xff0c;家政行业仍面对较…

graphics.h安装后依旧报错

问题解决一&#xff1a; 我在网上找了很多&#xff0c;都说找到graphics.h这个文件&#xff0c;放到include这个目录下&#xff0c;我照做了&#xff0c;然后 当我进行编译时&#xff0c;自动跳到graphics.h这个文件并出现一堆报错 问题解决二&#xff1a; 看一下这两个文件是…

Linux库之动态库静态库

一、什么是库&#xff08;Library&#xff09; 二、库的分类 三、静态库、动态库优缺点 四、静态库的制作和使用 五、动态库的制作和使用 SO-NAME–解决主版本号之间的兼容问题 基于符号的版本机制 共享库系统路径 共享库的查找过程 有用的环境变量 gcc 编译器常用选项 Linux共…

STM32F1外部中断EXTI

目录 1. EXTI简介 2. EXTI基本结构 3. AFIO复用IO口 4. EXTI框图 5. EXTI程序配置 5.1 首先先配置要使用的GPIO口的引脚 5.2 配置AFIO数据选择器&#xff0c;选择想要中断的引脚 5.3 EXTI配置 1. EXTI简介 EXTI&#xff08;Extern Interrupt&#xff09;外部中…

思腾云计算中心 | 5千平米超大空间,基础设施完善,提供裸金属GPU算力租赁业务

2021年&#xff0c;思腾合力全资收购包头市易慧信息科技有限公司&#xff0c;正式开启云计算业务。思腾云计算中心占地2400平米&#xff0c;位于包头市稀土高新区&#xff0c;毗邻多家知名企业&#xff0c;地理位置优越&#xff0c;交通便利&#xff0c;是区内重要的信息化产业…

配置集群免密登录

文章目录 前言配置集群免密登录1. 设置主机名与 IP 地址的映射关系2. 生成 SSH 密钥对3. 将公钥复制到集群节点4. 测试免密登录5. 配置节点之间互相免密登录 总结 前言 本文介绍了如何配置集群之间免密登录&#xff0c;以便在搭建集群环境时方便地进行节点之间的通信。通过设置…