- 目标
- 前言
- 源码分析
- 侵入式业务代码形式
- holder 的设计
- ttl 设置值
- ttl 获取值
- TtlRunnable 实现
- 核心run 方法
- 捕获方法(capture)
- 重放方法(replay)
- 为什么需要在重放中移除不存在快照中的子线程已经存在的ttl
- 恢复方法(restore)
- 为什么需要恢复方法(restore)
- 特别注意的点
- 实现一个线程安全的ttl
- 其他
- 总结
-
了解 TransmittableThreadLocal (可传递本地线程)源码
-
了解一些应用场景
参考:https://github.com/alibaba/transmittable-thread-local 官方源码地址
从TransmittableThreadLocal使用前调研(源码分析) 推荐分析到位
之前想实现一个分布式链路追踪,考虑一次请求链路的TranceId,spanId 要在整当前微服务下整个方法调用中传递,考虑使用Threadlocal 和 InheritableThreadLocal 来实现存放TranceId和spanId 。但是在线程池的场景下,会复用线程池线程,会出现丢失和污染调用链路的TranceId和spanId。需要解决这个问题,我尝试通过Java agent 的形式去修改java线程池源码的方式,来实现在执行前捕获父线程的threadlocal信息。在之前线程方法时使用。但是,改造点有点多,后来不了了之。
最近看一些日志记录的追踪文章,提到了TransmittableThreadLocal 框架提供了在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。所以,需要看下这个框架的实现。
源码分析官方地址:https://github.com/alibaba/transmittable-thread-local
在官方文档上介绍,使用TTL 有两种形式,
第一种是使用封装好的修饰类, TransmittableThreadLocal 来在线程池中传递上下文,TtlRunnable 和 TtlCallable 来代替java原生的 Runable 和 Callable 。当然也可以使用修饰的线程池
通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法: getTtlExecutor:修饰接口Executor getTtlExecutorService:修饰接口ExecutorService getTtlScheduledExecutorService:修饰接口ScheduledExecutorService
第二种方式是使用java agent 的形式,不入侵业务代码,通过修改字节码的技术,来实现的。第二种方式,放在下一篇描述。
侵入式业务代码形式核心源码:com.alibaba.ttl.TransmittableThreadLocal 继承了 InheritableThreadLocal
// 继承于 InheritableThreadLocal public class TransmittableThreadLocalextends InheritableThreadLocal implements TtlCopier {
声明了一个类变量holder(类变量在当前jvm下,只会存在一个) , 特别注意 holder 是 InheritableThreadLocal 类型, 在我们操作 InheritableThreadLocal 变量时(set 或者 get)是会从当前线程去获取的, holder 的作用存一个线程中所有出家门和口岸的的 TransmittableThreadLocal
特别说明: holder 存放了一个 weakhashmap ,这个map的key为 TransmittableThreadLocal 并且value 为null。
WeakHashMap 修饰弱引用的键,当某个弱键不再被强引用,那么下一次GC ,将会回收弱引用对象,那么弱键对应的键值对,将会被WeakHashMao 移除, 而在这里修饰的弱键,就是 TransmittableThreadLocal。为了防止内存泄露
private static final InheritableThreadLocalholder 的设计, ?>> holder = new InheritableThreadLocal , ?>>() { // 重写了 initialValue 方法 @Override protected WeakHashMap , ?> initialValue() { return new WeakHashMap<>(); } // 重写了 childValue 方法 @Override protected WeakHashMap , ?> childValue(WeakHashMap , ?> parentValue) { return new WeakHashMap , Object>(parentValue); } };
参考:http://events.jianshu.io/p/79650be29e34
来记录某个线程所有的TransmittableThreadLocal对象,线程级别的缓存。
主线程需要将TransmittableThreadLocal传递到子线程,那么主线程如何记录所有的TransmittableThreadLocal?
使用 InheritableThreadLocal 存放了一个WeakHashMap ,其中key 存放的TransmittableThreadLocal,value 默认为null
ttl 设置值先看 Ttl 设置一个上下文变量会有哪些步骤
@Override
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && null == value) {
// may set null to remove value
remove();
} else {
// 调用父类的set 方法 threadloacl
super.set(value);
// 添加当前对象到 holder 持有者
addThisToHolder();
}
}
@SuppressWarnings("unchecked")
private void addThisToHolder() {
// 不存在该key,就存入,判断当前创建的 TransmittableThreadLocal 实例对象没有被加入,就存入holder
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal
ttl 获取值
@Override
public final T get() {
// 调用父类的get,本质上就是从当前线程的 threadlocal 中获取值
T value = super.get();
if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
return value;
}
TtlRunnable 实现
从上面可知,Ttl 的设置值和获取值过程。那么从TtlRunnable 中,来看如果实现的父子线程中的 Ttl 设置的值传递。
核心run 方法
@Override
public void run() {
// 获取快照对象
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
// 重放
final Object backup = replay(captured);
try {
runnable.run();
} finally {
// 恢复
restore(backup);
}
}
三个重要操作,capture,replay,restore,本质执行线程任务,还是 runnable.run() 提供的,只是在执行线程任务前后做了增强。
捕获方法(capture)构造方法:
// 从构造看,私有的,runnable 是真正执行线程任务的 TtlRuannable 只是包装了一次
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
// 初始化 捕获当前 父线程设置的 threadlocal 的快照对象
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
capture() 方法, 在构造方法的第一行,就是去捕获父线程设置的 ttl 信息,用于自己使用
而 capture 是Ttl 类下的一个静态方法
// 捕获当前线程中的所有 TransmittableThreadLocal 和注册的 ThreadLocal 值。
@NonNull
public static Object capture() {
// 创建一个快照,第一个方法捕获ttl
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
private static HashMap, Object> captureTtlValues() {
// 创建一个
HashMap, Object> ttl2Value = new HashMap<>();
// 从当前holder 中获取存放的 TransmittableThreadLocal
for (TransmittableThreadLocal
上述 threadLocal.copyValue() 默认实现如下:
会有线程安全问题,也就是浅拷贝和深拷贝的问题。
浅拷贝可以简单理解为传递的是对象引用,修改了对象属性,本身对象就会被修改。
深拷贝可以简单理解为新创建一个对象,将现有对象属性赋值到新对象上,传递新对象使用。新对象的属性修改,不涉及现有对象。
private T copyValue() {
return copy(get());
}
public T copy(T parentValue) {
return parentValue;
}
所以在实现中,可以重写此方法,实现深拷贝。
public static TransmittableThreadLocal重放方法(replay)
@NonNull
public static Object replay(@NonNull Object captured) {
final Snapshot capturedSnapshot = (Snapshot) captured;
//
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
@NonNull
private static HashMap, Object> replayTtlValues(@NonNull HashMap, Object> captured) {
HashMap, Object> backup = new HashMap<>();
// 循环当前线程的在hodler 中持有的 ttl (简单的理解就是 子线程之前持有的ttl)
for (final Iterator> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal
为什么需要在重放中移除不存在快照中的子线程已经存在的ttl
https://github.com/alibaba/transmittable-thread-local/issues/134 解决在此
简单理解,从功能和系统设计上,如果在父线程就没有设置过ttl内容,就贸然出现在子线程。这就是bug
以下是作者的说明:
按上面分析,总结一下:
- 没有captured的值 不能/不应该 传递,否则 是 Bug。
肯定 不期望数据 以不确定的方式 出现在 子线程中。 - 要传递哪些数据,是由业务(逻辑)来决定 / 全权控制(通过在 *合适位置* 调用ThreadLocal#set方法来控制)。
并没有 你说的『数据丢失』的问题
恢复方法(restore)// 将子线程本身ttl 恢复
public static void restore(@NonNull Object backup) {
final Snapshot backupSnapshot = (Snapshot) backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}
private static void restoreTtlValues(@NonNull HashMap, Object> backup) {
// call afterExecute callback
doExecuteCallback(false);
for (final Iterator> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal
为什么需要恢复方法(restore)
解答在此:http://events.jianshu.io/p/79650be29e34
https://github.com/alibaba/transmittable-thread-local/issues/190
https://github.com/alibaba/transmittable-thread-local/issues/201
作者说了两种场景:
- 上面提到的场景,线程池满了 且 线程池使用的是『CallerRunsPolicy』,
则 提交到线程池的任务 在capture线程直接执行,也就是 直接在业务线程中同步执行; - 使用ForkJoinPool(包含并行执行Stream与CompletableFuture,底层使用ForkJoinPool)的场景,展开的ForkJoinTask会在调用线程中直接执行。
简单理解,当父线程调用子线程任务,但是子线程无法工作,继续由父线程完成子线程工作。如果没有恢复方法,父线程自身的ttl也就丢失了
作者也指出:
CRR(Capture/Replay/Restore)是一个面向上下文传递设计的流程,通过这个流程的分析可以保证/证明 正确性。
这个正确性的分析/证明 ,不依赖于 局部与反例。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),new ThreadPoolExecutor.CallerRunsPolicy());
特别注意的点
http://events.jianshu.io/p/79650be29e34 参考此篇文章,这部分内容直接copy 此篇文章
请参考此文中第三点
也可以参见blibli这位up的避坑
https://www.bilibili.com/video/BV1Nv411H7kt?spm_id_from=333.337.search-card.all.click
实现一个线程安全的ttl
private static final TransmittableThreadLocal> sealThreadLocalEnv = new TransmittableThreadLocal>() {
@Override
protected Map initialValue() {
return new LinkedHashMap<>();
}
//解决普通线程池中使用TTL,造成数据污染的问题
@Override
protected Map childValue(Map parentValue) {
return initialValue();
}
//父子线程使用的是拷贝对象。而非简单对象的引用。
@Override
public Map copy(Map parentValue) {
return new LinkedHashMap<>(parentValue);
}
};
其他
Thread 初始化inheritableThreadLocals 的传递
// 构造方法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
// 获取父线程对象
Thread parent = currentThread();
// 省略部分代码
// 判断父线程对象 inheritableThreadLocals(父子线程传递的threadlocal)是否为空
if (parent.inheritableThreadLocals != null)
// 将本线程的 inheritableThreadLocals 属性指向了 新创建的 ThreadLocalMap 包含父线程的 inheritableThreadLocals 的内容数据
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 创建一个ThreadLocalMap 入参是 父线程的 inheritableThreadLocals
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
总结
看TTL源码实现,通过自定义的ttl存放本地变量和增强版的Runnable 实现。通过捕获,重放,恢复方法,保证了既要传递父线程的ttl,并且保证子线程自身就存在的ttl不丢失。不过注意点,如果传递时引用对象,会存在线程安全问题。需要重写copy 和 childValue 方法。下一篇,将分析使用java agent方式不侵入业务代码形式的TransmittableThreadLocal 源码。



