文章目录

  • 目标
  • 前言
  • 源码分析
  • 侵入式业务代码形式
  • holder 的设计
  • ttl 设置值
  • ttl 获取值
  • TtlRunnable 实现
  • 核心run 方法
  • 捕获方法(capture)
  • 重放方法(replay)
  • 为什么需要在重放中移除不存在快照中的子线程已经存在的ttl
  • 恢复方法(restore)
  • 为什么需要恢复方法(restore)
  • 特别注意的点
  • 实现一个线程安全的ttl
  • 其他
  • 总结


目标

前言

之前想实现一个分布式链路追踪,考虑一次请求链路的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(包含并行执行StreamCompletableFuture,底层使用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 源码。