聊聊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中,大致相当于这样:

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

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

但真的是这样吗?

实际上,如果你点进createMap()会发现:

t.threadLocals其实是Thread内部的ThreadLocalMap,这里正在给Thread的ThreadLocalMap赋值呢,而且ThreadLocalMap的key是this,也就是当前ThreadLocal,而不是Thread。

是不是打破三观,甚至觉得有点懵?没关系,下面才是正文开始,会慢慢解释ThreadLocal的来龙去脉。

如何理解ThreadLocal是一个钩子?

如上图,Thread t1被实例化后,其实内部有个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,其中Thread的成员变量map指向堆中新建的ThreadLocalMap。

你可以理解为:

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)塞回孙悟空胸膛

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

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

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

多个Thread与多个ThreadLocal

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

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

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

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

悟空去取西经了,杀青了,暂时先忘了他。

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

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

如果你顺着箭头看,会发现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为什么要把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

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

相关推荐
李慕婉学姐5 分钟前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆2 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin2 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20052 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉2 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国3 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882483 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈3 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_993 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹3 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理