再谈谈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

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

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值