个人查看了很多书籍以及博科介绍,发现对于该类的介绍都比较笼统抽象。所以在此整理个人的学习理解!
问题引入:
为什么要用ThreadLocal,什么是ThreadLocal?
我们考虑这样一个问题:现在有两个线程,这两个线程都要访问一个变量,那么为了解决多线程并发下的同步问题,就要对这个变量进行同步处理。但是,我们要求访问的这个变量,不要求同步,也就是说,多线程访问一个变量的时候,怎么做到每一个线程对该变量的操作,其他线程不可见?
比如说一个变量,初始为10,其中一个线程把其修改为了20,但是怎么样保证,其他线程看到的还是10?
顺着这个问题,我们可以有这样一种思路,每一个线程访问这个变量的时候,都得到一个该变量的副本,所有的操作都是针对自己内部的这个副本进行操作,而不影响其他线程的操作!也就是所谓的保证不可见!
这时候,我们的主角ThreadLoacl就登场了。
ThreadLocal,线程本地变量。ThreadLocal为共享变量在每个线程中都创建了一个副本,每个线程都可以访问自己内部的副本变量。各个线程之间的变量互不干扰。
这里强调一点,所谓的副本(其实你看完这篇文章,就发现副本的说法并不是很准确,此处用这个说法,只是为了和上述的问题所提到的副本的思路所照应),其实是ThreadLocal在每个执行线程中,维护了一个ThreadLocal为键,具体存储内容为值的键值结构,所以副本并不是说,复制了一个ThreadLocal,而是通过共享的ThreadLocal去找到线程内部维护仅自己可见的值。
下面,源码分析,很容易就可以理解其底层实现的原理。
看到这里,其实可以理解这样一个事实,那就是,ThreadLocal并不是为了解决多线程对共享变量的同步问题,而是为了让每个线程都访问其内部的一个只属于自己的结构,互相不影响,由于没有多线程竞争问题,自然也没有性能损耗。
那么ThreadLocal的底层是什么样的呢?
在此,我们根据一个简单例子,进行引入,然后根据JDK的源码进行深入理解:
public class ThreadLocalTest {
public static ThreadLocal<Integer> tl = null;
public static void main(String args[])
{
tl = new ThreadLocal<Integer>()
{ //重写初始化变量方法
protected Integer initialValue()
{
return 10;
}
};
ThreadTest tt1 = new ThreadTest(20);
ThreadTest tt2 = new ThreadTest(30);
Thread th1 = new Thread(tt1);
Thread th2 = new Thread(tt2);
//控制逻辑,让th1整个执行完,th2才开始
th1.start();
try {
th1.join(); //当前主线程mian停留在这里,直到th1线程执行完毕后,main线程继续执行
} catch (InterruptedException e) {
e.printStackTrace();
}
th2.start();
}
}
//封装线程类,实现接口,实现方法
class ThreadTest implements Runnable
{
private int num = 0;
public ThreadTest(int num)
{
this.num = num;
}
public void run()
{
//线程本地变量的初始值
System.out.println(Thread.currentThread() + "初始:" + ThreadLocalTest.tl.get());
ThreadLocalTest.tl.set(this.num);
//上面这一步是核心,也就是当前线程的set方法,其他线程不可见。因为操作的实际上是一个线程本地的副本。
System.out.println(Thread.currentThread() + "更改为:" + ThreadLocalTest.tl.get());
}
}
测试结果:
记住线程所指的这个数值,等下分析的时候,理解这一步,就是ThreadLocal所实现的意义。
下面一点一点分析上述例子:
注意,例子中的ThreadLocal变量名为tl,使用的泛型为integer。也就是说,tl内部保存着一个Integer的数值。
借用方腾飞前辈《java并发编程的艺术》一书中的描述:ThreadLocal,即线程变量,是一个以ThreadLocal为键,任意对象(取决于程序中的泛型)为值的存储结构,这个结构被附带在线程上,也就是说一个线程可以根据一个Threadlocald对象查询到绑定在这个线程上的一个值!
看线程内部具体实现,图中所示的三个箭头所指的方法。
get()方法:得到tl中所保存的默认值,或者如果当前线程使用set()方法设置过,就得到设置的值!
set()方法:当前线程设置tl在当前线程中的副本的值!
所以第一个get得到10,set设置为this.num,在主函数中可以看到具体num值,然后第二个get得到set设置的值!
然后看线程的执行顺序,其中使用到了join方法,强制第二个线程开始,必须等在第一个线程执行结束。
再结合线程类的num初值,不难分析出程序执行结果。
第一个线程开始执行,第一次get为初始值10(这个初始值的问题后续会说明)。然后set设置20,然后get得到20.等到第一个线程执行结束,第二个执行开始(注意这时候,重点来了)看第二个线程第一次get的值,仍为10,说明了什么?正如开头所介绍的,说明了第二个线程的第一次get得到的是ThreadLocal的默认初始值,而不是第一个线程set的值。这里就说明了,线程间的Threadlocal在多线程并发间,互相不可见,不影响。
重点来了,那为什么会这样呢?怎么实现的呢?让我们根据JDK源码一点一点脱掉这一层层神秘的外套,去了解底层的设计!
(1)
第一层,ThreadLocal的构造函数是默认无参构造函数,且方法体为空!
再来看看我们之前例子中,new一个ThreadLocal的时候:
(2)
这时候,应该理解,这一步仅仅是实例化了一个ThreadLocal的对象实体,并没有做其他任何额外的工作,因为构造函数采用的是默认无参的,什么都没有做,所以这里并没有为ThreadLocal内部保存的Integer赋予指定的值。而只是重写了一下一个名为initialValue的方法。
那为什么第一个线程的第一次get就得到了10呢?不是还没有把10赋给Integer吗?
那我们来看一下get()方法的源码:
(3)
一点点分析,首先get()方法得到当前正在执行ThreadLocald的get()方法的线程,也就是图片中所示的第一条语句的作用。然后又出现了一个getMap()方法,该方法传入参数为第一条语句得到的当前线程。
这里也应该清晰的看到,get方法内部维护着一个Map。(该map后续会看到源码)
好了,不着急,我们继续看getMap(Thread t)方法:
(4)
分析:该方法传入一个线程t,然后返回线程t的一个threadLocals变量,类型为ThreadLocalMap;
(5)
上述代码为该变量在Thread类中的初始定义。也就是说,线程类Thread内部有一个成员变量,类型为ThreadLocalMap,变量名为threadLocals.且初始值为null。并且Thread的构造函数并没有实例化这个成员变量。
好了,往回退:(根据代码块右下角,我标注的序号来往回找)
回到(4),我们知道此时返回的是 null。
回到(3),map==null,所以if进不去,直接执行return setInitialValue();
不着急,静下心,继续看这个方法:
(6)
好了,进到这个方法后,第一步就是initiaValue,是不是很眼熟,没错,就是咱们一开始重写的这个方法。
我们先来看看,JDK中默认的该方法:
(7)
很尴尬,竟然直接给了个null。从这里体现出了一个细节问题,那就是,如果我们想要初始化这个变量(例子中是Integer)的值,一定要重写这个初始化的方法,因为默认的是直接给了个null,没有任何值的。
回到(2),看我们重写的方法,返回了整数10.
然后,我们可以看到又是getMap方法,很显然,仍然是null、所以看第二个箭头所指的语句,这时候调用了createMap方法,从方法名不难看出,应该是要创建一个map了。我们来看源码:
(8)
果不其然,new 出了一个ThreadLocalMap的实例,两个参数,一个是this,也就是ThreadLocal,一个是Value值。
在看map构造之前,有必要提出一点,看上述代码的等号左边,这个map是在t中维护的,而t是传入的当前执行线程。相信不用多说,也可以稍微理解这样一个事实,ThreadLocal在每个线程中都有一个副本,这个副本是线程自己内部维护的一个map,所以ThreadLocal内部维护的Integer,是自己可见的,其他线程也就获取不到了。
然后,我们来看ThreadlocalMap的源码:
(9)
这一段代码,有个巧妙的亮点,就是这个弱引用,其内部存储实体结构Entry<ThreadLocal, T>
继承自java.lan.ref.WeakReference
,这样当ThreadLocal
不再被引用时,因为弱引用机制原因,当jvm发现内存不足时,会自动回收弱引用指向的实例内存,即其线程内部的ThreadLocalMap
会释放其对ThreadLocal
的引用从而让jvm回收ThreadLocal
对象。这里重点强调下,是回收ThreadLocal
对象,而非整个Entry
,所以线程变量中的值T
对象还是在内存中存在的,所以内存泄漏的问题还没有完全解决。
再来看ThreadLocalMap的构造方法:
(10)
根据(9)和(10)就可以很容易的看出,这个ThreadLocalMap内部就是关于Entry的散列表。而且每一个Entry内部都维护这一个具体的Value。注意的是,这个ThreadLocalMap就是JDK单独为ThreadLocal写的,并没有实现java.util.map,只不过也是采用的散列的思想而已。
这里作者当时学习的时候,产生了一个很大的问题:既然每一个线程内部维护着这样的一个ThreadLocalMap,保证了各个线程间对该map的分离不可见,那只有一个Entry是有用的,就是那个ThreadLocal对应的Value,这个map岂不是在浪费资源吗?
其实这个问题很傻很简单,一个ThreadLocal对应一个Value,那如果该程序中有很多ThreadLocal呢?所以这才是实现这个map的意义,当时考虑这个问题惯性思维了,一直受到了例子中只有一个ThreadLocal的影响。哈哈!
返回(8),返回(6),可以从(6)看到,第一次get返回的就是我们重写函数返回的值10,并且创建出了map。
该map的层次是,当前执行线程内部的一个threadLocals变量,类型为ThreadLocalMap,其中保存着<ThreadLocal,Integer>的一种存储结构。
好了,分析完第一次执行get方法后,我们再来进行set方法源码的学习:
(11)
可以看到,set也是拿到当前执行线程所维护的ThreadLocalMap,然后通过map.set()方法更新其内部维护的值,由于这个map是自己内部维护的,所以其他线程不可见,这也是ThreadLocal的实现核心。
然后,我们再来看看get方法,分析第二次执行get方法的过程,
此时,map不为null了,然后根据当前ThreadLocal也就是代码中的this,获取Entry,从Entry中得到value值!
理解到此,相信,如果你静下心来,一步一步推敲,对Threadlocal的实现已经胸有成竹了!哈哈!