聊聊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的正经用法:

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
进群,大家一起学习,一起进步,一起对抗互联网寒冬

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值