作者简介:大家好,我是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
进群,大家一起学习,一起进步,一起对抗互联网寒冬