009-ThreadLocal、InheritableThreadLocal和TransmittableThreadLocal的实现原理
一、ThreadLocal介绍
ThreadLocal介绍
threadLocal的特点就是与线程绑定,一般通过这种隐式传参的方式来传递上下文。
典型场景例子:
- 用户登录和获取用户相关信息,这时候如果在每个需要用户信息的方法入参上加入用户信息参数就先的非常冗余,不够优雅。
- 日志的链路信息等等。
1.1 使用方式
使用方式比较简单,一般是在入口处设置,然后在后续方法中获取使用
private static ThreadLocal<String> USER_CONTEXT = new ThreadLocal<>();
...
try {
USER_CONTEXT.set("username");
String userInfo = USER_CONTEXT.get();
System.out.println(userInfo);
} finally {
USER_CONTEXT.remove();
}
1.2 set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 获取当前线程
- 获取Map(ThreadLocalMap)
- 将值设置到Map
ThreadLocalMap存放在哪?ThreadLocal中吗?
答:显然不是,存放在Thread中。原因是在于如果将ThreadLocalMap维护在ThreadLocal中,那么显然在Thread执行完毕后,需要去清除掉ThreadLocal中存储的信息,如果忘记或者因为特殊原因线程中断掉了,那么将会导致ThreadLocal中数据不会被释放。如果是存放于Thread中,那么维护问题就解决了,线程不管是正常还是异常关闭,其中使用得内存都会得到释放。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Thread中的成员变量
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
get方法也是一样,把ThreadLocal对象当做Key去Thread的ThreadLocalMap中获取Value。
1.3 问题点
但是这样就意味着,我们无法使用多线程来处理任务,因为set的时候只在当前Thread的ThreadLocalMap中存放了我们的数值,如果在创建一个新的Thread的话,那么显然其中的Map不会有我们设置的值。
USER_CONTEXT.set("username");
Thread newThread = new Thread(() -> System.out.println(USER_CONTEXT.get()));
其中newThread打印出来的就是null
这时候聪明的小伙伴就发现,那我在创建新线程的时候将上一个线程的ThreadLocalMap带过来不就行了嘛,这就是我们要讨论的第二个类InheritableThreadLocal
二、InheritableThreadLocal介绍
根据上面的问题我们知道ThreadLocal存在的局限性,InheritableThreadLocal也就是可继承的ThreadLocal,这里的可继承就是指的ThreadLocalMap。
它实现可继承的方式,就如前文描述的一样,在创建新线程的时候,将当前线程的ThreadLocalMap设置到新线程中,但不再是threadLocals成员属性,而是inheritableThreadLocals成员属性。
2.1 InheritableThreadLocal类源码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
//获取Map变成了inheritableThreadLocals
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
//创建Map变成了inheritableThreadLocals
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
2.2 线程间传值实现原理
说到InheritableThreadLocal,还要从Thread类说起:
public class Thread implements Runnable {
......(其他源码)
/*
* 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
* 主要用于父子线程间ThreadLocal变量的传递
* 本文主要讨论的就是这个ThreadLocalMap
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......(其他源码)
}
Thread类中包含 threadLocals 和 inheritableThreadLocals 两个变量,其中 inheritableThreadLocals 即主要存储可自动向子线程中传递的ThreadLocal.ThreadLocalMap。
接下来看一下父线程创建子线程的流程,我们从最简单的方式说起:
Thread thread = new Thread()
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
/**
* 默认情况下,设置inheritThreadLocals可传递
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
/**
* 初始化一个线程.
* 此函数有两处调用,
* 1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true
* 2、传递AccessControlContext,inheritThreadLocals=false
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
......(其他代码)
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
......(其他代码)
}
可以看到,采用默认方式产生子线程时,inheritThreadLocals=true;若此时父线程inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。
继续追踪ThreadLocal.createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
/**
* 构建一个包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap
* 该函数只被 createInheritedMap() 调用.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
// ThreadLocalMap 使用 Entry[] table 存储ThreadLocal
table = new Entry[len];
// 逐一复制 parentMap 的记录
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 可能会有同学好奇此处为何使用childValue,而不是直接赋值,
// 毕竟childValue内部也是直接将e.value返回;
// 个人理解,主要为了减轻阅读代码的难度
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
从ThreadLocalMap可知,子线程将parentMap中的所有记录逐一复制至自身线程。
总结
InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。
2.3 问题点
InheritableThreadLocal可以在子线程创建的时候,将父线程的本地变量拷贝到子线程中。
那么问题就来了,是只有在创建的时候才拷贝,只拷贝一次,然后就放到线程中的inheritableThreadLocals属性缓存起来。由于使用了线程池,该线程可能会存活很久甚至一直存活,那么inheritableThreadLocals属性将不会看到父线程的本地变量的变化
InheritableThreadLocal 只有在父线程创建子线程时,在子线程中才能获取到父线程中的线程变量;当配合线程池使用时:“第一次在线程池中开启线程,能在子线程中获取到父线程的线程变量,而当该子线程开启之后,发生线程复用,该子线程仍然保留的是之前开启它的父线程的线程变量,而无法获取当前父线程中新的线程变量”,所以会发生获取线程变量错误的情况。
/**
* 测试线程池下InheritableThreadLocal线程变量失效的场景
*/
public class TestInheritableThreadLocal {
private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
// 固定大小的线程池,保证线程复用
private static final ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
threadLocal.set("main线程 变量1");
// 正常取到 main线程 变量1
executorService.execute(() -> System.out.println(threadLocal.get()));
threadLocal.set("main线程 变量2");
// 线程复用再取还是 main线程 变量1
executorService.execute(() -> System.out.println(threadLocal.get()));
}
}
那么对于我们日常开发中使用线程池的这种方式,有现成的解决方案吗?
有的,阿里开源TransmittableThreadLocal【github地址:https://github.com/alibaba/transmittable-thread-local】
InheritableThreadLocal的思路是在创建的时候进行拷贝。
而TransmittableThreadLocal的思路则是在运行前(run()之前拷贝)
三、TransmittableThreadLocal介绍
TransmittableThreadLocal(TTL)是阿里开源的用于解决,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
3.1 需要解决的问题
在探究源码之前,我们需要明确使用的场景,以及场景所产生的问题。
- 在线程之中建立或使用另一个线程,并且需要继承当前线程的上下文
- 建立或使用线程,第一立即使用,第二某段时间后使用(线程池提交但不马上执行)
- 执行任务的线程也存在两种情况,第一新线程(其它线程),第二当前线程(由池化特性决定)
对于第三种情况,以线程池为例来说,如果拒绝策略为CallerRunsPolicy,也就是用提交的线程来执行,那么就存在第二种情况,由当前线程执行
接下来,梳理下可能面临的问题点(以线程池为例)
1、何时进行当前线程上下文的获取?
由于上面的场景存在延时执行,那么获取上下文就只能在新线程创建的时候,对于使用其它线程(线程池存在的线程)就只能是创建任务的时候。
2、如何拷贝上下文?
对于引用对象来说,如果直接使用其地址,可能就存在问题,外层会影响到执行线程的信息,这需要根据业务场景来确定,是否能影响。
3、对于当前线程执行的情况,如何保证上下文不丢失?
这种情况出现在,当我们提交的任务被划分的线程有自己的上下文(任务的提交和实际执行中间存在时间差,如果这个时间段出现了上下文的更新,那么直接覆盖将导致本次更新丢失),那么就需要保证在任务执行的时候是当时的上下文,执行完毕后需要还原。
4、什么时候设置上下文?
由于前面我们知道,在任务提交和执行存在一定的时间差,那么设置上下文的时候,就不能是创建的时候,只能是在执行之前(如果在创建的时候,还需要考虑,中途如果没轮到该任务执行就设置了上下文,线程如果还有其它的流程需要执行,就会导致上下文丢失问题)
3.2 源码分析
TTL整体是通过装饰器模式,来对现有的线程池,Runable进行增强。
3.2.1 最简单的使用方式:
//使用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();
3.2.2 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;
}
3.2.3 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;
}
这里的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;
还有一个就是成员变量holder。这个holder中存放了所有TransmittableThreadLocal的引用,而拷贝其实就是将TransmittableThreadLocal的引用和当时其中的值拷贝(取决于拷贝的方式,对于引用类型要考虑是否能受外层影响)到capturedRef成员变量中,这样TtlRunnable就能在运行时获取到上下文了。
下图在总体流程上描述new TtlRunnable()的整个过程
到此拷贝已经完成,接下来就是使用前进行值设置。
3.2.4 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);
}
第一步也就是上面我们刚聊过的,就不过多赘述了。
第二步也就是设置上下文
第三步设置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;
}
对于1,2步骤,主要是把执行时刻的快照保存下来,等执行完后在设置会去,如果有点迷糊可以看下【问题三】
backup解决的场景,提交执行的线程有自己的上下文(场景比较少,但是情况确实存在)
到这里TransmittableThreadLocal大体执行流程就分析完毕,还涉及到的一些方法可以深入源码中去查看下。