• 聊聊ThreadLocal(一)


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

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

    话说《中华英雄》有一个情节就是华英雄远赴美国,结果被卖到采石场做苦力。后来联合鬼仆师兄还有采石场的其他朋友,大闹了一场。所以本篇文章开头,打算自己画个漫画,纪念一下逝去的童年时光:

    咳咳,扯远了。今天我们来聊聊ThreadLocal。至于这个漫画,自有我的用意。

    内容介绍:

    • ThreadLocal初印象
    • 如何调戏ThreadLocalMap
    • ThreadLocal简单使用
    • ThreadLocal源码解析
    • ThreadLocal、ThreadLocalMap、Thread三者关系
    • 彩蛋
    • 待扩展内容

    ThreadLocal初印象

    网上已经有很多ThreadLocal相关的博文,我个人获益颇多,但还是不够直观。尤其对于“一个线程可以有多个threadLocal”的说法,有那么一段时间让我感到很困惑…

    自学JavaWeb时,在JDBC一章崔老师曾引入ThreadLocal实现对connection对象的管理。按崔老师的说法,可以将ThreadLocal理解为一个大Map,key是每个线程,value是Object类型。每个线程访问该Map时,只能取到与本线程绑定的变量,从而做到线程隔离。

    虽然这个说法不是很准确,但还是非常直观的。而且据说在ThreadLocal早期版本中,确实是这样实现的:ThreadLocal内部塞了一个Map,以线程作为key。ThreadLocal本身不存东西。

    但不知道从哪一版开始,ThreadLocal的实现已经做了修改。从JDK1.8的源码来看,ThreadLocalMap的key不再是线程,而是ThreadLocal对象。

    你肯定很好奇,以前用线程作为key,每个线程访问Map得到与自己绑定的value,很合理。现在改用ThreadLocal对象作为key,每个线程如何知道哪个键值对属于自己?这里先按下不表。

    为了让大家对ThreadLocal的内部实现有个快速、直观的认识,我画了一张图:

    ThreadLocal的静态内部类:ThreadLocalMap。而Thread中有个成员变量threadLocals可以指向它

    为了方便理解,可以把ThreadLocal看成是一个工具箱,里面提供了一系列操作容器(ThreadLocalMap)的方法:get、set、remove...


    如何调戏ThreadLocalMap

    看到这里,我们已经知道ThreadLocal之所以能存东西,是因为里面有个ThreadLocalMap。那如果我们能直接得到ThreadLocalMap实例,就能撇开ThreadLocal自己玩了。没有中间商赚差价,岂不妙哉?

    通过阅读源码,我们发现ThreadLocalMap是ThreadLocal的内部类,而且是静态内部类。这就非常easy了啊。想当年我们学JavaSE的时候,内部类也没少玩。先自己试试看:

    我们发现,可以直接通过 new Outer.Inner()方式实例化静态内部类,稳得一批。真开心,终于可以不理会ThreadLocal,自己单干了:

    结果发现压根不行...

    看了错误提示才恍然大悟:ThreadLocal虽然是public权限,但是静态内部类ThreadLocalMap只是默认权限。如果一个包下的类想要供其他包的类使用,那么这个类必须是public,不论是普通类还是内部类。很遗憾,ThreadLocalMap并不是:

    既不能通过外部类实例化ThreadLocalMap,又无法直接new ThreadLocalMap():

    那么,ThreadLocalMap对于我们来说,就是完全限制访问的,只能当它不存在...

    也就说,ThreadLocal通过给ThreadLocalMap使用默认的权限修饰符,使得ThreadLocalMap无法被其他包的类引用,最终将ThreadLocalMap完美地隐藏在java.lang包内部。

    所以结论是,我们无法直接调戏ThreadLocalMap,只能通过ThreadLocal明媒正娶。


    ThreadLocal简单使用

    放弃不正规的渠道,接下来看一下ThreadLocal的正经用法:

    1. public class TestThreadLocal {
    2. //创建两个ThreadLocal实例并指定泛型,分别存储Long/String类型数据
    3. private static ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    4. private static ThreadLocal<String> stringLocal = new ThreadLocal<String>();
    5. //set方法,因为只是内部调用,用了private
    6. private void set() {
    7. longLocal.set(Thread.currentThread().getId());
    8. stringLocal.set(Thread.currentThread().getName());
    9. }
    10. //get方法
    11. private long getLong() {
    12. return longLocal.get();
    13. }
    14. //get方法
    15. private String getString() {
    16. return stringLocal.get();
    17. }
    18. public static void main(String[] args) throws InterruptedException {
    19. //------main线程执行开始--------
    20. final TestThreadLocal test = new TestThreadLocal();
    21. test.set();
    22. System.out.println(test.getLong());
    23. System.out.println(test.getString());
    24. Thread thread = new Thread() {
    25. public void run() {
    26. //-------Thread-0线程执行开始--------
    27. test.set();
    28. System.out.println(test.getLong());
    29. System.out.println(test.getString());
    30. //-------Thread-0线程执行结束--------
    31. }
    32. };
    33. thread.start();
    34. //thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
    35. thread.join();
    36. System.out.println(test.getLong());
    37. System.out.println(test.getString());
    38. //------main线程执行结束--------
    39. }
    40. }

    两个线程执行示意图:main,Thread-0

    输出结果:

    main线程两次打印的中途,Thread-0线程开启并调用了test.set()进行设置。main线程和Thread-0设置的值肯定不同,但最终main线程前后打印结果一致。也就是说,main线程和Thread-0是线程隔离的,变量相互独立。


    ThreadLocal源码分析

    ThreadLocal为什么能做到线程隔离呢?我们来看一下完整的类结构:

    左边是工具(ThreadLocal),右边是容器(ThreadLocalMap)

    看了上面的类结构后,我们回顾一下上面看过的图:

    这次感觉亲切多了吧?

    虽然ThreadLocal提供的方法很多,但常用的大部分方法会在get()和set()中被调用,所以我们只分析这两个方法。

    另外,请注意,内部类其实本质上和普通类差不多,内部类实例和外部类实例之间也并不存在继承。只不过ThreadLocalMap的情况稍微特殊一些,由于权限问题,我们必须通过ThreadLocal间接操作它。

    所以稍后画示意图时,我更倾向于把TheadLocalMap单独抽出来,画成下面这样:

    ThreadLocalMap内部也有个静态内部类:Entry,用来装键值对

    set源码图解

    其实就是华英雄向包工头要箩筐的代码实现。包工头给了华英雄一个箩筐,华英雄放了(set)一块砖进去。整体比较简单,有几点注意一下即可:

    1.线程对象刚创建时,threadLocals肯定还未赋值,所以是null

    2.在ThreadLocal的set()中,调用getMap(currentThread)得到当前线程的threadLocals。如果发现当前线程尚未绑定ThreadLocalMap实例,ThreadLocal会创建一个Map并绑定。此时,Thread中的threadLocals指向新创建的ThreadLocalMap实例

    3.ThreadLocalMap创建的table可以看成一个哈希表,默认大小是16,即有16个槽(slot)。创建table完毕,根据firstKey算出本次插入的槽位,然后用内部类Entry将两个值包装成键值对(entry),放入槽中:table[i] = new Entry(firstKey, firstValue);

    get源码图解

    上面是华英雄第二次访问包工头(ThreadLocal)的代码实现。包工头发现他已经有箩筐(ThreadLocalMap),所以不再分配新的箩筐,于是华英雄找到自己的箩筐,拿到了之前set进去的砖头。

    但这是非常理想化的场景。现在我们来设想一下:倘若在set之前,先get,会发生什么呢?

    会有以下两种可能:

    1.ThreadLocalMap还未初始化:箩筐都没有,如何得到砖?

    做了三件事:

    • 创建map
    • 给map设置一个键值对{threadLocal : initialValue}
    • 返回initialValue,默认null

    2.ThreadLocalMap已经初始化,但是map中没有查到这个key:有箩筐,但是没找到想要的那块砖

    做了两件事:

    • 往map里设置键值对{threadLocal : initialValue}
    • 返回initialValue,默认null

    set之后get,会得到刚才set的值。而在set之前就get会产生两种情况,但两种情况唯一的差异在于是否创建map,共同点则是:不管新Map还是旧Map,由于之前没有set值,所以此次get肯定是取不到值的。但总要给个返回结果吧?ThreadLocalMap的做法是往Map中插入键值对{this ThreadLocal : initialValue},然后返回initialValue。也就是说,取不到值就统一返回默认值。

    为什么不直接返回默认值,还要多加一步插入entry的操作?因为这样下次你就能找到值了…

    但是要注意,initialValue默认是null:

    如果我们后续还有操作,可能会发生空指针异常,所以推荐创建ThreadLocal对象时,复写initialValue():


    ThreadLocal、ThreadLocalMap、Thread三者关系

    我知道,上面的源码分析未必能让大家对ThreadLocal有个全局的认识。因为Thread/ThreadLocal/ThreadLocalMap的关系实在太乱了。接下来做一下整理:

    1.虽然ThreadLocalMap是ThreadLocal的静态内部类,但它们的实例对象并不存在继承或者包裹关系。完全可以当成两个独立的实例。

    2.ThreadLocal的作用有两个

    • 工具类,提供一系列方法操作ThreadLocalMap,比如get/set/remove
    • 隔离Thread和ThreadLocalMap,防止程序员直接创建ThreadLocalMap(无法调戏)。但自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap。有就继续用,没有就为其绑定

    现在,让我们回到华英雄的故事。

    为了防止工人随意占用箩筐(ThreadLocalMap),采石场的箩筐统一交给包工头(ThreadLocal)管理(设计成内部类且不给public权限)。虽然箩筐在包工头手里,但是分发给工人(Thread)后,这个箩筐就和工人绑定了,和包工头没太大关系。

    所以本质上,ThreadLocal和Thread没有必然联系。哪怕再来几个工人,只要他确实还没有箩筐,包工头都会给他一个。

    另外,采石场那么多工人,包工头是不会去记自己的箩筐给过哪位工人的。但工人每次去访问包工头时,包工头都会问他是否已经有箩筐,有的话就用自己现有的箩筐搬石头。至于工人现有的箩筐是不是自己当初发的,重要吗?

    所以回到文章开头我困惑的那句:一个线程可以有多个threadLocal。就会发现这句话,好像有道理,又好像完全没道理。因为它俩并不存在“谁拥有谁”的关系。实在要说的话,应该是一个工人(Thread)只能有一个箩筐(ThreadLocalMap)。


    现在,把一开始的程序示意图画一遍:


    彩蛋

    最后,还有个小彩蛋,由于不知道放哪,就放这儿了。

    我们在调戏ThreadLocalMap时,发现外部无法直接创建它。但是后面分析源码时,我们发现ThreadLocal都是调用createMap()创建的。所以,贼心不死的我想看看是否可以直接通过threadLocal.createMap()创建:

    错误提示:非public的method无法被不同包下的类调用...和内部类的权限问题一样。

    ThreadLocal虽然设计了createMap(),但并没打算给外部调用。所以并没有给createMap()加public。

    而是通过对外暴露public void set()和public T get(),并在方法内加入判断,使得在满足条件时才能为线程创建ThreadLocalMap实例。

    答应我,放弃调戏ThreadLocalMap!!!

    (不愧是JDK源码,设计得真好...)


    待扩展内容

    上文的replaceStaleEntry()继续往下分析,会发现ThreadLocalMap本身有清除“废弃槽”的机制。所谓“废弃槽”,是我自己乱翻译的:比如某个ThreadLocal对象已经被回收,那么key = null,对应的value再也用不了。这种“废弃槽”多了以后,会浪费内存,甚至造成内存溢出。

    另外,Entry继承了WeakReference,将自己的key包装为弱引用。

    ---------------------------------------------------------------------------------------------------------------------------------

    作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
    进群,大家一起学习,一起进步,一起对抗互联网寒冬

  • 相关阅读:
    微服务介绍
    探秘数据库中间件:ProxySQL与MaxScale的优势与劣势
    Redis——新数据类型
    509 - RAID! (UVA)
    Makefile
    [C语言刷题篇]链表运用讲解
    MVP-3:登陆注册自定义token拦截器
    尼尔·唐纳德·沃尔什《与神对话》
    YOLOv5结合华为诺亚VanillaNet Block模块
    突破编程_C++_高级教程(正则表达式编程实例)
  • 原文地址:https://blog.csdn.net/smart_an/article/details/134454770