一提到ThreadLocal,很多人都会心惊胆战,因为ThreadLoca确实算是JAVA并发编程中一大难点,尤其是ThreadLocal、ThreadLocalMap、Thread三者之间的关系更是错综复杂。本文结合JAVA源码,皆以最简单的方式介绍这三者概念以及它们之间的关系,希望能帮读者打通这其中的任督二脉。
之前一直有个观念,学习一个新的知识之前,必先了解其为什么诞生以及其解决的是什么问题。
之前我们学习多线程的时候就知道,多线程对共享变量进行访问,可以会出现安全问题。
ThreadLocal(线程变量)是解决线程安全问题一个很好的思路:它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,并发性更高。
在了解完ThreadLocal的基本概念之后,下面先用一张图展示ThreadLocal、ThreadLocalMap、Thread的关系。这里看不懂也没关系,下面会通过源码来介绍。
1.首先我们来看一下Thread类,可以看到里面有一个ThreadLocalMap类型的变量(这里有点坑,虽然这个变量名字叫threadLocals,但他是ThreadLocalMap类型)。
2.接下来我们看到ThreadLocal类,可以看到里面有一个ThreadLocalMap静态内部类,也就是上面Thread里面threadLocals变量将要指向的对象。
其实这里也是最容易迷惑的一点,即ThreadLocalMap是ThreadLocal类的静态内部类。
为什么要这样设计呢?
ThreadLocal通过给ThreadLocalMap使用默认的权限修饰符,使得ThreadLocalMap无法被其他包的类引用,最终将ThreadLocalMap完美地隐藏起来,同时ThreadLocal提供了一系列操作容器ThreadLocalMap的方法(get、set等),供外界使用。
通过这里我们可以小小总结一下:Thread类里面有一个ThreadLocalMap类型的变量,但是外界无法直接操作这个ThreadLocalMap,提供了一个工具箱ThreadLocal帮助我们操作ThreadLocal。
这样通过干巴巴的文字依然无法讲清楚这三者的关系,接下来我们看两个最重要的方法set 和 get看一下具体的流程。
我们先来看一下ThreadLocal中的set()方法,先获取当前线程,然后获取当前线程中的ThreadLocalMap(即threadLocals变量指向的),如果不为空,则把当前对象ThreadLocal作为键,外界传过来的参数作为值,保存ThreadLocalMap中。如果为空,则创建一个新的ThreadLocalMap。
接下来我们来看一下get方法,先获取当前线程对象,然后拿到当前线程对象的ThreadLocalMap,在根据键(即当前对象ThreadLocal)找到值。
如果我们在获取的时候当前线程对象的ThreadLocalMap为null时,则会执行setInitialValue()方法。
做了三件事:
1.创建map
2.给map设置一个键值对{threadLocal : initialValue}
3.返回initialValue,默认null
通过上面的源码我们可以知道,ThreadLocalMap以ThreadLocal作为键,值是我们调用ThreadLocal的set方法传进来的。
看到这里大家可能又乱了,这ThreadLocal、ThreadLocalMap、Thread关系到底是啥啊?现在来举个通俗易懂的例子。
例如:张三(线程1)和李四(线程2)去搬砖,有小白(ThreadLocal1)和小黑(ThreadLocal2)两个包工头,第一次它们报道找到了小白包工头,由于它们没有工资卡(ThreadLocalMap),小白给它们一人发了一张工资卡。第二天他们去找到了小黑包工头,可是他们已经有工资卡了,小黑就没有在给它们发工资卡(即一个线程只能有一个ThreadLocalMap)。
看到这个例子可以得出很多结论:
1.我们必须通过包工头拿到工资卡(即ThreadLocal是个工具箱,Thread要通过工具箱获取ThreadLocalMap)
2.一个人只能有一张工资卡(即一个Thread只能有一个ThreadLocalMap)。
3.对于张三和李四来说只需要拿到工资卡即可,至于是谁发的重要吗?(即无论使用哪个ThreadLocal创建ThreadLocalMap都不重要)。
但这个工资卡到底存储什么呢?
我们知道ThreadLocalMap以ThreadLocal为键,外界传入的value作为值,我们可以这样想:这个工资卡存储的就是为哪个包工头干了多少价值的活。
张三的工资卡可以为:小白(ThreadLocal1):200 ,小黑(ThreadLocal2):300。
李四的工资卡可以为:小白(ThreadLocal1):400 ,小黑(ThreadLocal2):500。
同时张三和李四可以去找更多的包工头干活,而他们两个并不会存在记混工资的情况,因为每个人都有每个人的工资卡呀(这就是线程隔离)!
从上面这一段再得出结论:
1.工资卡和包工头是独立的(虽然ThreadLocalMap是ThreadLocal的静态内部类,但完全可以当成两个独立实例)
2.工资卡以键值对形式存储数据,键是包工头,值是我们传进来的value。
1.ThreadLocal的作用有两个
1.工具类,提供一系列方法操作ThreadLocalMap,比如get/set/remove
2.隔离Thread和ThreadLocalMap,防止程序员直接创建ThreadLocalMap。
自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续用,没有就为其绑定。
2.虽然ThreadLocalMap是ThreadLocal的静态内部类,但它们的实例对象并不存在继承或者包裹关系。完全可以当成两个独立的实例。
3.一个Thread只能有一个ThreadLocalMap。
4.ThreadLocalMap以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 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 {
System.out.println("------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() {
System.out.println("-------Thread-0线程执行开始--------");
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
System.out.println("-------Thread-0线程执行结束--------");
}
};
thread.start();
//thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
thread.join();
System.out.println(test.getLong());
System.out.println(test.getString());
System.out.println("------main线程执行结束--------");
}
}
运行结果如下图所示:
main线程两次打印的中途,Thread-0线程开启并调用了test.set()进行设置。main线程和Thread-0设置的值肯定不同,但最终main线程前后打印结果一致。也就是说,main线程和Thread-0是线程隔离的,变量相互独立。
同时Thread-0线程和main线程共用了longLocal和stringLocal 两个ThreadLocal,但是却做到了线程隔离,可以看出线程隔离与ThreadLocal无关,ThreadLocal只是个工具人帮助操作而已,真正实现线程隔离的是线程内部唯一对应的那个ThreadLocalMap!
个人认为理解ThreadLocal、ThreadLocalMap、Thread的关键就是它们是相辅相成的独立个体,Thread需要ThreadLocalMap实现线程隔离,而操作ThreadLocalMap又需要ThreadLocal,就是这么简单而已。至于为什么ThreadLocalMap的键是ThreadLocal,可以这样想:你(Thread)通过我(ThreadLocal)实现了某种目的(线程隔离),总要留下我的身影吧!