TransmittableThreadLocal(TTL)
是阿里开源的用于解决,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
详细的内容可以查看 https://github.com/alibaba/transmittable-thread-local
通过上一篇文章我们了解到ThreadLocal
和Inheritable
所存在的局限性,针对这种局限性TTL
提出并实现了一种解决方案。
如果对于TTL
和上面所说的局限性,没有清晰概念的同学可以看下ThreadLocal与InheritableThreadLocal的实现原理和上面TTL
github上面的介绍
在探究源码之前,我们需要明确使用的场景,以及场景所产生的问题。
对于第三种情况,以线程池为例来说,如果拒绝策略为CallerRunsPolicy,也就是用提交的线程来执行,那么就存在第二种情况,由当前线程执行
接下来,梳理下可能面临的问题点(以线程池为例)
ps:对线程池理解不太明白的同学,可以看看我另一篇文章
ThreadPoolExecutor源码详解
如何拷贝上下文?
对于引用对象来说,如果直接使用其地址,可能就存在问题,外层会影响到执行线程的信息,这需要根据业务场景来确定,是否能影响。
对于当前线程执行的情况,如何保证上下文不丢失?
这种情况出现在,当我们提交的任务被划分的线程有自己的上下文(任务的提交和实际执行中间存在时间差,如果这个时间段出现了上下文的更新,那么直接覆盖将导致本次更新丢失),那么就需要保证在任务执行的时候是当时的上下文,执行完毕后需要还原。
什么时候设置上下文?
由于前面我们知道,在任务提交和执行存在一定的时间差,那么设置上下文的时候,就不能是创建的时候,只能是在执行之前(如果在创建的时候,还需要考虑,中途如果没轮到该任务执行就设置了上下文,线程如果还有其它的流程需要执行,就会导致上下文丢失问题)
最后,我们尝试画下时序图(以线程池为例,最常规的情况)
在对整体流程有了详细理解后,接下来就进行源码阅读
对于如何使用,可以查看官网中的使用方式,使用方式是比较简单的。
TTL整体是通过装饰器模式,来对现有的线程池,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();
根据步骤1提交任务(拷贝上下文),也就是在TtlRunnable.get()
方法中,最后就是new TtlRunnable()
。
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
//这就是去拷贝当前线程的上下文
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
这里出现了一个工具类Transmitter
,它主要的功能就是帮助我们拷贝,存放,重置ThreadLocal
信息,这里ThreadLocal
既可能是ThreadLocal
也可能是TransmittableThreadLocal
,接下来就先看看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()
的整个过程
到此拷贝已经完成,接下来就是使用前进行值设置。
使用前也就是在任务运行前
//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()
方法的必要性。
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
大体执行流程就分析完毕,还涉及到的一些方法可以深入源码中去查看下。
觉得不错的同学可以关注下我哟,不定期更新工作中遇到的问题以及解决方案(* ̄︶ ̄)