根据JDK源码理解学习ThreadLocal

个人查看了很多书籍以及博科介绍,发现对于该类的介绍都比较笼统抽象。所以在此整理个人的学习理解!

问题引入:

为什么要用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的实现已经胸有成竹了!哈哈!

猜你喜欢

转载自blog.csdn.net/romantic_jie/article/details/100165513