前言
之前把《现代操作系统》的前四章看完了,收获还是很大的,尤其在进程管理那一章让我明白了并发在操作系统上是如何控制的。所以,由此也引发了这一篇源码解析文章的创作。原本是打算写AQS的源码解析的,但是最近项目比较忙,没有时间,只能先写比较简单的ThreadLocal。
PS:最近都在看《计算机网络 谢希仁编著》 ,感觉跟之前看的完全不是一本书,就是怎么变得那么简单了,可能这就是境界上的提升吧,如果LOL的段位也有那么简单提升就好了,哈哈哈哈(可能是我依然没有领悟它的精髓)。
简单使用
ThreadLocal
的使用是非常简单的
public class TestThreadLocal {
private static ThreadLocal<String> local = new ThreadLocal<>();
private static void print(String t){
System.out.println(t + ":" + local.get());
}
public static void main(String[] args) {
Thread threadOne = new Thread(() -> {
local.set("threadOne local variable");
print("threadOne");
});
Thread threadTwo = new Thread(() -> {
local.set("threadTwo local variable");
print("threadTwo");
});
threadOne.start();
threadTwo.start();
}
}
复制代码
这段代码的输出结果就是
threadOne: threadOne local variable
threadTwo: threadTwo local variable
代码执行流程:
线程threadOne
通过local.set("threadOne local variable")
在线程内部设置了一个值,然后通过local.get()
可以获取到这个在线程内部的值。
线程threadTwo
同理
为什么在两个线程内部调用同一个local.get()
方法却可以获取到不同的值?
在解释这个问题之前,我们先来讨论另一个问题,通过local.set()
方法设置的值到底存放在哪里?
set方法
我们先来看一下java的源码,都是大佬写的,要细细的品,给大佬应有的尊重!
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
复制代码
虽说是大佬写的代码,但是从表面上来看,还是很简单的,哈哈哈哈!
我梳理一下这段代码都做了什么:
- 首先获取到执行该方法的线程,从上面使用的代码来看就是ThreadOne(我们只看一个线程,因为另一个线程也是一样的)。
- 然后将该线程传递给了
getMap(t)
方法,那我们就来看一下这段代码做了什么
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
复制代码
这段代码返回了线程对象中的threadLocals
变量。
纳尼???线程对象里面竟然还有这个变量?
没错,它确实存在着,如果你只阅读过Thread
类的源码,确实可能会把这个变量忽略,因为Thread
类中的绝大部分方法都是跟该变量无关的。
我使用的是jdk8,所以在Thread
类中的181行就是该变量的声明。
ThreaLocal
类维护的。这里的map就是指
ThreadLocalMap
。
ThreadLocalMap是一个定制的Map,实际的使用上跟我们的HashMap是类似的,只是多了一个内部类Entry,该类是一个弱引用类,这个会在下一篇的get方法讲,这里就把map当作是一个我们平时使用的Map就可以了。
所以我们大概可以回答上面提出的那个问题,值到底存放在哪里?从这里,我们知道值是存放在Thread
线程对象里面的。
实际上值是存在ThreadLocal的ThreadLocalMap里面,而map又存放在线程对象里面,所以间接的存放在线程对象里面
为什么要用Map来存呢?我觉得应该是因为一个线程对象可以有很多个ThreaLocal
对象。List也可以存多个值,但是使用List来存的话,我们取值时就不知道应该取哪一个值了。
让我们言归正传,继续看set方法
- 获取到map之后会进行判断:
如果map不为空,那么就将ThreadLocal对象作为键,参数value作为值,通过map的set()
方法进行设置。
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
复制代码
如果map为空,那么就对map进行初始化。
void createMap(Thread t, T firstValue) {
// 创建一个ThreadLocalMap,并且设置第一个键值对,等同于map.set(this, value)
// 然后赋值给当前线程对象的threadLocals变量
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码
map将ThreadLocal对象作为键,那我们就可以大胆猜测测试代码中local.get()
方法是怎么获取值的。
- 首先获取当前线程对象
- 然后获取当前线程中的threadLocals变量
- 因为threadLocals是一个以ThreadLocal对象作为键的map结构,所以我们只要将this(调用get()的ThreadLocal对象)作为参数传递到map的get方法中就可以获取到对应的值。
ThreadLocal的get()基本步骤是这样的,但是因为传递进来的value实际上是要被弱引用的,所以具体实现会有一些不同。至于弱引用我会在后面单独作为一个专栏来讲。
结论
我们通过set方法可以总结几点:
- ThreadLocal对象内部是通过Map结构存储传递进来的值的,以ThreadLocal对象作为键,vlaue作为值进行存储。
- Map结构是存储在线程对象内部的,所以我们获取值的时候先通过线程对象获取到Map,然后通过ThreadLocal作为键在Map中获取到值。
- 一个线程对象是可以有多个ThreadLocal对象的,但是一个ThreadLocal对象只能对应一个值。所以如果要在一个线程中复制多个全局变量的副本,就要创建多个ThreadLocal对象。
尾声
终于写完了,明天我会把get方法也一起写出来!如果有疑问的话,可以直接评论区一起讨论哦,知无不言。后续还会写出几篇与ThreadLocal有关的专栏,包括剩下的一些比较重要的方法,弱引用,应用场景,ThreadLocal如何造成内存泄漏。