作者简介:大家好,我是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的正经用法:
public class TestThreadLocal {
//创建两个ThreadLocal实例并指定泛型,分别存储Long/String类型数据
private static ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
private static ThreadLocal<String> stringLocal = new ThreadLocal<String>();
//set方法,因为只是内部调用,用了private
private void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
//get方法
private long getLong() {
return longLocal.get();
}
//get方法
private String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
//------main线程执行开始--------
final TestThreadLocal test = new TestThreadLocal();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread = new Thread() {
public void run() {
//-------Thread-0线程执行开始--------
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
//-------Thread-0线程执行结束--------
}
};
thread.start();
//thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
thread.join();
System.out.println(test.getLong());
System.out.println(test.getString());
//------main线程执行结束--------
}
}
两个线程执行示意图: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
进群,大家一起学习,一起进步,一起对抗互联网寒冬