ThreadLocal原理及使用
# 前言
在我进行多线程并发编程时,经常会遇到访问共享变量的问题。为了解决这一问题,我采用了ThreadLocal
这个强大的工具。
ThreadLocal意为线程本地变量,用于解决多线程并发时访问共享变量的问题。
所谓的共享变量指的是在堆中的实例、静态属性和数组;对于共享数据的访问受Java的内存模型(JMM)的控制,其模型如下:
【参考:《Java并发编程 (opens new window)的艺术》P22】
每个线程都会有属于自己的本地内存,在堆(也就是上图的主内存)中的变量在被线程使用的时候会被复制一个副本线程的本地内存中,当线程修改了共享变量之后就会通过JMM管理控制写会到主内存中。
很明显,在多线程的场景下,当有多个线程对共享变量进行修改的时候,就会出现线程安全问题,即数据不一致问题。常用的解决方法是对访问共享变量的代码加锁(synchronized或者Lock)。但是这种方式对性能的耗费比较大。在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题。
当然锁和ThreadLocal使用场景还是有区别的,具体区别如下:
synchronized(锁) | ThreadLocal | |
---|---|---|
原理 | 同步机制采用了时间换空间的方式,只提供一份变量,让不同线程排队访问(临界区排队) | 采用空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
# 一、ThreadLocal的使用及原理
# 1.1 使用
- 一般都会将ThreadLocal声明成一个静态字段,同时初始化如下:
static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
其中Object就是原本堆中共享变量的数据。
注意事项
- 一个线程需要设置多个独立的值,单个
ThreadLocal
是不够用的,因为每个ThreadLocal
实例只能为每个线程存储一个数据值。 - 最直接的方法是为每个需要独立存储的值创建一个
ThreadLocal
实例,这种方法简单直接,易于理解和使用。
例如,有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下:
public class Test {
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
2
3
- 常用的方法
- set(T value):设置线程本地变量的内容。
- get():获取线程本地变量的内容。
- remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
public class Test {
static ThreadLocal<User> threadLocal = new ThreadLocal<>();
public void m1(User user) {
threadLocal.set(user);
}
public void m2() {
User user = threadLocal.get();
// 使用
// 使用完清除
threadLocal.remove();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.2 原理
那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?首先,在Java中的线程是什么呢?是的,就是一个Thread类的实例对象!而一个实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存ThreadLocal线程本地变量作为一个Thread类的成员字段,这个成员字段就是:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
2
3
是一个在ThreadLocal中定义的Map对象,保存了该线程中的所有本地变量。ThreadLocalMap中的Entry的定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// key为一个ThreadLocal对象,v就是我们要在线程之间隔离的对象
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2
3
4
5
6
7
8
9
ThreadLocalMap和Entry都在ThreadLocal中定义。
ThreadLocal::set方法的原理
set方法的源码如下:
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals字段
ThreadLocalMap map = getMap(t);
// 判断线程的threadLocals是否初始化了
if (map != null) {
map.set(this, value);
} else {
// 没有则创建一个ThreadLocalMap对象进行初始化
createMap(t, value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
createMap方法的源码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2
3
map.set方法的源码如下:
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
// map中就是使用Entry[]数据保留所有的entry实例
Entry[] tab = table;
int len = tab.length;
// 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 已经存在则替换旧值
e.value = value;
return;
}
if (k == null) {
// 在设置期间清理哈希表为空的内容,保持哈希表的性质
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 扩容逻辑
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Thread::get方法的原理
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取ThreadLocal对应保留在Map中的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取ThreadLocal对象对应的值
T result = (T)e.value;
return result;
}
}
// map还没有初始化时创建map对象,并设置null,同时返回null
return setInitialValue();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ThreadLocal::remove()方法原理
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
// 键在直接移除
if (m != null) {
m.remove(this);
}
}
2
3
4
5
6
7
ThreadLocalMap的类结构体系如下:
# 1.3 ThreadLocal设计
- 在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,但是现在已经不是这样了。
- JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:
- 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
- Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
- 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。
JDK8之后设计的好处在于:
- 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
- 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。
使用ThreadLocal的好处
- 保存每个线程绑定的数据,在需要的地方可以直接获取,避免直接传递参数带来的代码耦合问题;
- 各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
# 二、ThreadLocal内存泄露问题
内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。
ThreadLocal的内存泄露问题一般考虑和Entry对象有关,在上面的Entry定义可以看出ThreadLocal::Entry被弱引用所修饰。**JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。**这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上并不是使用了弱引用就A会发生内存泄露问题,考虑下面几个过程:
- 使用强引用
当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说就算使用了强引用仍然会出现内存泄露问题。
使用弱引用
当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。
由此可以发现,使用ThreadLocal造成内存泄露的问题是因为:ThreadLocalMap的生命周期与Thread一致,如果不手动清除掉Entry对象的话就可能会造成内存泄露问题。
因此,需要我们在每次在使用完之后需要手动的remove掉Entry对象。
那么为什么使用弱引用?
避免内存泄露的两种方式:使用完ThreadLocal,调用其remove方法删除对应的Entry或者使用完ThreadLocal,当前Thread也随之运行结束。第二种方法在使用线程池技术时是不可以实现的。
所以一般都是自己手动调用remove方法,调用remove方法弱引用和强引用都不会产生内存泄露问题,使用弱引用的原因如下:
在ThreadLocalMap的set/getEntry中,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
总结:存在内存泄露的有两个地方:ThreadLocal和Entry中Value;最保险还是要注意要自己及时调用remove方法!!!
# 三、ThreadLocal的应用场景
场景一:在重入方法中替代参数的显式传递
假如在我们的业务方法中需要调用其他方法,同时其他方法都需要用到同一个对象时,可以使用ThreadLocal替代参数的传递或者static静态全局变量。这是因为使用参数传递造成代码的耦合度高,使用静态全局变量在多线程环境下不安全。当该对象用ThreadLocal包装过后,就可以保证在该线程中独此一份,同时和其他线程隔离。
? 例如在Spring的@Transaction事务声明的注解中就使用ThreadLocal保存了当前的Connection对象,避免在本次调用的不同方法中使用不同的Connection对象。
场景二:全局存储用户信息
可以尝试使用ThreadLocal替代Session的使用,当用户要访问需要授权的接口的时候,可以现在拦截器中将用户的Token存入ThreadLocal中;之后在本次访问中任何需要用户用户信息的都可以直接冲ThreadLocal中拿取数据。例如自定义获取用户信息的类AuthHolder:
public class AuthNHolder {
private static final ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();
public static void map(Map<String,String> map){
threadLocal.set(map);
}
// 获取用户id
public static String userId(){
return get("userId");
}
// 根据键值获取对应的信息
public static String get(String key){
Map<String,String> map = getMap();
return map.get(key);
}
// 用完清空ThreadLocal
public static void clear(){
threadLocal.remove();
}
}
12345678910111213141516171819
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
备注:参考博文https://cloud.tencent.com/developer/article/1636025。ThreadLocal里面封装的value只是一个例子,根据具体业务需求改就行了。
场景三:解决线程安全问题
依赖于ThreadLocal本身的特性,对于需要进行线程隔离的变量可以使用ThreadLocal进行封装。
# 四、ThreadLocal总结
- ThreadLocal更像是对其他类型变量的一层包装,通过ThreadLocal的包装使得该变量可以在
线程之间隔离
和当前线程全局共享
。 - 线程的隔离性和变量的线程全局共享性得益于在每个Thread类中的threadlocals字段。(从类实例对象的角度抽象的去看Java中的线程!!!)
- ThreadLocalMap中Entry的Key不管是否使用弱引用都有内存泄露的可能。引起内存泄露主要在于ThreadLocal对象和Entry中的Value对象,因此要确保每次使用完之后都remove掉Entry!
# 五、InheritableThreadLocal详解
# 1. InheritableThreadLocal可以做什么
提示
实现在子线程中使用父线程中的线程本地变量
我们知道ThreadLocal (opens new window)解决的是让每个线程读取的ThreadLocal变量是相互独立的。通俗的讲就是,比如我在线程1中set了ThreadLocal的值,那我在线程2中是get不到线程1设置的值的,只能读到线程2自己set的值。
ThreadLocal有一个需求不能满足:就是子线程无法直接复用父线程的ThreadLocal变量里的内容。demo如下:
public class TestThreadLocal implements Runnable {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
System.out.println("----主线程设置值为\"主线程\"");
threadLocal.set("主线程");
System.out.println("----主线程设置后获取值:" + threadLocal.get());
Thread tt = new Thread(new TestThreadLocal());
tt.start();
System.out.println("----主线程结束");
}
@Override
public void run() {
System.out.println("----子线程设置值前获取:" + threadLocal.get());
System.out.println("----新开线程设置值为\"子线程\"");
threadLocal.set("子线程");
System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果:
可以看到虽然在main线程中启动了一个新的子线程,但是threadlocal变量的内容并没有传递到新的子线程中。
于是乎,InheritableThreadLocal就出现了。他可以实现在子线程中使用父线程中的线程本地变量(也即InheritableThreadLocal变量)。
# 2. InheritableThreadLocal使用实例
demo,根据上面的threadlocal测试代码稍作修改,把Threadlocal换做InheritableThreadLocal。
public class TestInheritableThreadLocal implements Runnable {
private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
System.out.println("----主线程设置值为\"主线程\"");
threadLocal.set("主线程");
System.out.println("----主线程设置后获取值:" + threadLocal.get());
Thread tt = new Thread(new TestInheritableThreadLocal());
tt.start();
System.out.println("----主线程结束");
}
@Override
public void run() {
System.out.println("----子线程设置值前获取:" + threadLocal.get());
System.out.println("----新开线程设置值为\"子线程\"");
threadLocal.set("子线程");
System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果如下:
在子线程设置值之前,就已经能够get到主线程设置的值了,说明在父子进制之间传递了InheritableThreadLocal变量。
# 3.InheritableThreadLocal原理
通过观察InheritableThreadLocal代码Structure,看到只是重写了ThreadLocal的三个方法。 childValue,createMap,getMap。
我们进入到createMap方法中查看。
可以看到,InheritableThreadLocal其实也是用ThreadLocalMap去存放值,这点和ThreadLocal一样,只不过InheritableThreadLocal的变量在Thread类里的名字叫inheritableThreadLocals。我们进到Thread类中看这个变量。
当我们在主线程start一个子线程时,会new 一个Thread。所以我们要追到Thread类中,看看创建线程时发生了什么才让父子线程的InheritableThreadLocal可以传递。
首先我们调用的是Thread(Runnable target)这个方法。
这个方法会调用init方法,然后经过一系列init函数重载,最终来到下面这个init方法。
在这个init方法里 ,跟InheritableThreadLocal紧密相关的有下面这些代码:
重点就是if里面的逻辑。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
第一项inheritThreadLocals 是传进来的boolean值,重载时传的是true,所以满足条件。
第二项就是判断父线程中的inheritableThreadLocals 是不是空,如果不是空就满足条件。
当同时满足if的两个条件后,就执行
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
新创建出来的子线程的inheritableThreadLocals 变量就和父线程的inheritableThreadLocals 的内容一样了。
以上就是从源码的角度分析InheritableThreadLocal的原理。
# 4.InheritableThreadLocal和线程池搭配使用的问题
首先给出结论:
InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果,因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。
下面是DEMO:
package com.mt;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestInheritableThreadLocalAndExecutor implements Runnable {
private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws Exception{
System.out.println("----主线程启动");
inheritableThreadLocal.set("主线程第一次赋值");
System.out.println("----主线程设置后获取值:" + inheritableThreadLocal.get());
executorService.submit(new TestInheritableThreadLocalAndExecutor());
System.out.println("主线程休眠2秒");
Thread.sleep(2000);
inheritableThreadLocal.set("主线程第二次赋值");
executorService.submit(new TestInheritableThreadLocalAndExecutor());
executorService.shutdown();
}
@Override
public void run() {
System.out.println("----子线程获取值:" + inheritableThreadLocal.get());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
运行结果:
从上图可以看出,我们在main线程中第二次set并没有被第二次submit的线程get到。也印证了我们的结论。 那么对于我们日常开发中使用线程池的这种方式,有现成的解决方案吗? 有的,阿里开源TransmittableThreadLocal【github地址:https://github.com/alibaba/transmittable-thread-local】
- InheritableThreadLocal的思路是在创建的时候进行拷贝。
- 而TransmittableThreadLocal的思路则是在运行前(run()之前拷贝)
# 六、TransmittableThreadLocal详解
上面我们了解到ThreadLocal
和Inheritable
所存在的局限性,针对这种局限性TTL
提出并实现了一种解决方案。
TransmittableThreadLocal(TTL)
是阿里开源的用于解决,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
# 需要解决的问题
在探究源码之前,我们需要明确使用的场景,以及场景所产生的问题。
- 在线程之中建立或使用另一个线程,并且需要继承当前线程的上下文
- 建立或使用线程,存在两种情况,第一立即使用,第二某段时间后使用(线程池提交但不马上执行)
- 执行任务的线程也存在两种情况,第一新线程(其它线程),第二当前线程(由池化特性决定)
对于第三种情况,以线程池为例来说,如果拒绝策略为CallerRunsPolicy,也就是用提交的线程来执行,那么就存在第二种情况,由当前线程执行
接下来,梳理下可能面临的问题点(以线程池为例)
何时进行当前线程上下文的获取? 由于上面的场景存在延时执行,那么获取上下文就只能在新线程创建的时候,对于使用其它线程(线程池存在的线程)就只能是创建任务的时候。
如何拷贝上下文? 对于引用对象来说,如果直接使用其地址,可能就存在问题,外层会影响到执行线程的信息,这需要根据业务场景来确定,是否能影响。
对于当前线程执行的情况,如何保证上下文不丢失? 这种情况出现在,当我们提交的任务被划分的线程有自己的上下文(任务的提交和实际执行中间存在时间差,如果这个时间段出现了上下文的更新,那么直接覆盖将导致本次更新丢失),那么就需要保证在任务执行的时候是当时的上下文,执行完毕后需要还原。
什么时候设置上下文? 由于前面我们知道,在任务提交和执行存在一定的时间差,那么设置上下文的时候,就不能是创建的时候,只能是在执行之前(如果在创建的时候,还需要考虑,中途如果没轮到该任务执行就设置了上下文,线程如果还有其它的流程需要执行,就会导致上下文丢失问题)
最后,我们尝试画下时序图 (opens new window)(以线程池为例,最常规的情况)
在对整体流程有了详细理解后,接下来就进行源码阅读
# 源码探究
对于如何使用,可以查看官网中的使用方式,使用方式是比较简单的。
TTL整体是通过装饰器模式 (opens new window),来对现有的线程池,Runable进行增强。
最简单的使用方式:
//使用TTL
TransmittableThreadLocal<Map<String, Integer>> USER_CONTEXT=new TransmittableThreadLocal<>();
//将普通的Runable包装成TtlRunnable
TtlRunnable ttlRunnable = TtlRunnable.get(() -> {
System.out.println(USER_CONTEXT.get().get("username"));
});
new Thread(ttlRunnable).start();
2
3
4
5
6
7
# TtlRunnable.get()
根据步骤1提交任务(拷贝上下文),也就是在TtlRunnable.get()
方法中,最后就是new TtlRunnable()
。
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
//这就是去拷贝当前线程的上下文
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
2
3
4
5
6
这里出现了一个工具类Transmitter
,它主要的功能就是帮助我们拷贝,存放,重置ThreadLocal
信息,这里ThreadLocal
既可能是ThreadLocal
也可能是TransmittableThreadLocal
,接下来就先看看Transmitter
。
# Transmitter类
其中主要有几个方法需要关注,分别是
- 拷贝上下文
capture()
- 存放上下文
replay()
- 重置上下文
restore()
实际的实现类是Transmittee
,保存在
private static final Set<Transmittee<Object, Object>> transmitteeSet = new CopyOnWriteArraySet<>();
总共有两个,一个处理ThreadLocal
,一个处理TransmittableThreadLocal
。
这里就以第二个的拷贝为例,详细代码可以去TransmittableThreadLocal.class
中查看:
public HashMap<TransmittableThreadLocal<Object>, Object> capture() {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<>(holder.get().size());
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
//这里需要注意下,确定copyValue()的拷贝方式
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
2
3
4
5
6
7
8
这里的copyValue()
方法需要注意下,目前是直接使用value对象,如果是引用对象,那么就会受外层的影响。如果想进行深拷贝,需要使用SuppliedTransmittableThreadLocal
类。
通过TransmittableThreadLocal.withInitialAndCopier()
方法,提供对应拷贝方法
private static final class SuppliedTransmittableThreadLocal<T> extends TransmittableThreadLocal<T> {
private final Supplier<? extends T> supplier;
private final TtlCopier<T> copierForChildValue;
private final TtlCopier<T> copierForCopy;
2
3
4
还有一个就是成员变量holder
。这个holder
中存放了所有TransmittableThreadLocal
的引用,而拷贝其实就是将TransmittableThreadLocal
的引用和当时其中的值拷贝(取决于拷贝的方式,对于引用类型要考虑是否能受外层影响)到capturedRef
成员变量中,这样TtlRunnable
就能在运行时获取到上下文了。
下图在总体流程上描述new TtlRunnable()
的整个过程
到此拷贝已经完成,接下来就是使用前进行值设置。
# TtlRunnable.run()
使用前也就是在任务运行前
//1. 获取快照,也就是Snapshot()
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
//2. 将快照中的值设置到当前线程的上下文中(也就是TransmittableThreadLocal或者ThreadLocal)
//3. 返回backup,就是在设置之前,当前线程的快照信息
final Object backup = replay(captured);
try {
runnable.run();
} finally {
//4.将设置的当前线程快照信息给重新设置回去
restore(backup);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
第一步也就是上面我们刚聊过的,就不过多赘述了。 第二步也就是设置上下文 第三步设置backup
接下来我们来看下replay()
方法的源码,以及第三步backup
和第四步restore()
方法的必要性。
# replay()
public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
final HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<>(holder.get().size());
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
//1.获取当前线程,上线文快照
backup.put(threadLocal, threadLocal.get());
//2.如果当前线程有快照里面不存在的上下文,那么先清除掉
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
//3.将创建TtlRunnable时保存的快照设置到当前线程的上下文中
setTtlValuesTo(captured);
//4.保留的一个hook用于自定义
doExecuteCallback(true);
//5.返回保存的快照
return backup;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
对于1,2步骤,主要是把执行时刻的快照保存下来,等执行完后在设置会去,如果有点迷糊可以看下【问题三】
backup
解决的场景,提交执行的线程有自己的上下文(场景比较少,但是情况确实存在)
当前线程线程池执行线程上下文为1提交任务进行拷贝拷贝上下文为1等待线程执行设置自己上下文2执行任务backup保存当前上下文2replay设置拷贝的上下文1执行任务使用Ttl 1restore重置上下文2执行完毕当前线程线程池执行线程
到这里TransmittableThreadLocal
大体执行流程就分析完毕,还涉及到的一些方法可以深入源码中去查看下。