文章目录
- 目标
- 前言
- 源码分析
- 侵入式业务代码形式
- holder 的设计
- ttl 设置值
- ttl 获取值
- TtlRunnable 实现
- 核心run 方法
- 捕获方法(capture)
- 重放方法(replay)
- 为什么需要在重放中移除不存在快照中的子线程已经存在的ttl
- 恢复方法(restore)
- 为什么需要恢复方法(restore)
- 特别注意的点
- 实现一个线程安全的ttl
- 其他
- 总结
目标
- 了解 TransmittableThreadLocal (可传递本地线程)源码
- 了解一些应用场景
参考:https:///alibaba/transmittable-thread-local 官方源码地址
从TransmittableThreadLocal使用前调研(源码分析) 推荐分析到位
前言
之前想实现一个分布式链路追踪,考虑一次请求链路的TranceId,spanId 要在整当前微服务下整个方法调用中传递,考虑使用Threadlocal 和 InheritableThreadLocal 来实现存放TranceId和spanId 。但是在线程池的场景下,会复用线程池线程,会出现丢失和污染调用链路的TranceId和spanId。需要解决这个问题,我尝试通过Java agent 的形式去修改java线程池源码的方式,来实现在执行前捕获父线程的threadlocal信息。在之前线程方法时使用。但是,改造点有点多,后来不了了之。
最近看一些日志记录的追踪文章,提到了TransmittableThreadLocal 框架提供了在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。所以,需要看下这个框架的实现。
源码分析
官方地址:https:///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 TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {声明了一个类变量holder(类变量在当前jvm下,只会存在一个) , 特别注意 holder 是 InheritableThreadLocal 类型, 在我们操作 InheritableThreadLocal 变量时(set 或者 get)是会从当前线程去获取的, holder 的作用存一个线程中所有存放的 TransmittableThreadLocal
特别说明: holder 存放了一个 weakhashmap ,这个map的key为 TransmittableThreadLocal 并且value 为null。
WeakHashMap 修饰弱引用的键,当某个弱键不再被强引用,那么下一次GC ,将会回收弱引用对象,那么弱键对应的键值对,将会被WeakHashMao 移除, 而在这里修饰的弱键,就是 TransmittableThreadLocal。为了防止内存泄露
private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
// 重写了 initialValue 方法
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<>();
}
// 重写了 childValue 方法
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
}
};holder 的设计
参考:http://events.jianshu.io/p/79650be29e34
来记录某个线程所有的TransmittableThreadLocal对象,线程级别的缓存。
主线程需要将TransmittableThreadLocal传递到子线程,那么主线程如何记录所有的TransmittableThreadLocal?
使用 InheritableThreadLocal 存放了一个WeakHashMap ,其中key 存放的TransmittableThreadLocal,value 默认为null
ttl 设置值
先看 Ttl 设置一个上下文变量会有哪些步骤
/**
* {@inheritDoc}
*/
@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<Object>) this, null); // WeakHashMap supports null value.
}
}ttl 获取值
@Override
public final T get() {
// 调用父类的get,本质上就是从当前线程的 threadlocal 中获取值
T value = super.get();
if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
return value;
}TtlRunnable 实现
从上面可知,Ttl 的设置值和获取值过程。那么从TtlRunnable 中,来看如果实现的父子线程中的 Ttl 设置的值传递。
核心run 方法
/**
* run 方法
*/
/**
* wrap method {@link Runnable#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!");
}
// 重放
/**
* 1. backup(备份)是子线程已经存在的ThreadLocal变量;
* 2. 将captured的ThreadLocal值在子线程中set进去;
*/
final Object backup = replay(captured);
try {
runnable.run();
} finally {
/**
* 在子线程任务中,ThreadLocal可能发生变化,该步骤的目的是
* 回滚{@code runnable.run()}进入前的ThreadLocal的线程
*/
// 恢复
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<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
// 创建一个
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<>();
// 从当前holder 中获取存放的 TransmittableThreadLocal
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
// 注意copyValue 方法, 调用了 threadLocal 的get 方法,并且使用了 copy 方法
// copy 方法默认实现是 parentvalue 是引用,直接获取过来了。(不是新建了一个对象)(会有线程安全问题,当传递一个对象时)
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}上述 threadLocal.copyValue() 默认实现如下:
会有线程安全问题,也就是浅拷贝和深拷贝的问题。
浅拷贝可以简单理解为传递的是对象引用,修改了对象属性,本身对象就会被修改。
深拷贝可以简单理解为新创建一个对象,将现有对象属性赋值到新对象上,传递新对象使用。新对象的属性修改,不涉及现有对象。
private T copyValue() {
return copy(get());
}
public T copy(T parentValue) {
return parentValue;
}所以在实现中,可以重写此方法,实现深拷贝。
public static TransmittableThreadLocal<Map> ttl = new TransmittableThreadLocal<Map>() {
@Override
public Map copy(Map parentValue) {
HashMap<Object, Object> objectObjectHashMap = new HashMap<>();
return objectObjectHashMap;
}
};重放方法(replay)
@NonNull
public static Object replay(@NonNull Object captured) {
final Snapshot capturedSnapshot = (Snapshot) captured;
//
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
/**
* 重放 ttl 的值
* @param captured
* @return
*/
@NonNull
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<>();
// 循环当前线程的在hodler 中持有的 ttl (简单的理解就是 子线程之前持有的ttl)
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
// backup 存放之前持有的所有 ttl
backup.put(threadLocal, threadLocal.get());
// clear the TTL values that is not in captured
// 清除未捕获的 TTL 值
// avoid the extra TTL values after replay when run task
// 避免运行任务时重放后的额外 TTL 值
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// set TTL values to captured
// 设置从父线程中获取的 ttl 设置到子线程中
setTtlValuesTo(captured);
// call beforeExecute callback
doExecuteCallback(true);
return backup;
}为什么需要在重放中移除不存在快照中的子线程已经存在的ttl
https:///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<TransmittableThreadLocal<Object>, Object> backup) {
// call afterExecute callback
doExecuteCallback(false);
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
// clear the TTL values that is not in backup
// avoid the extra TTL values after restore
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// restore TTL values
setTtlValuesTo(backup);
}为什么需要恢复方法(restore)
解答在此:http://events.jianshu.io/p/79650be29e34
https:///alibaba/transmittable-thread-local/issues/190
https:///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<Map<String, String>> sealThreadLocalEnv = new TransmittableThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue() {
return new LinkedHashMap<>();
}
//解决普通线程池中使用TTL,造成数据污染的问题
@Override
protected Map<String, String> childValue(Map<String, String> parentValue) {
return initialValue();
}
//父子线程使用的是拷贝对象。而非简单对象的引用。
@Override
public Map<String, String> copy(Map<String, String> 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 源码。
















