【TransmittableThreadLocal】TransmittableThreadLocal的实现机制和原理

1  前言

前面我看过了 ThreadLocal的实现机制和原理 以及 InheritableThreadLocal的实现机制和原理 两种类型的 ThreadLocal,前者是普通的,后者是在前者的基础上套了一层父子线程关系,当使用后者的时候,会在线程创建的时候,浅拷贝一份父线程的变量值。那么今天空了,我来看看另外一种 ThreadLocal:TransmittableThreadLocal。

2  TransmittableThreadLocal

2.1  TransmittableThreadLocal 的认识

TransmittableThreadLocal(TTL)是阿里巴巴开源的一个 Java 库,用于解决 ThreadLocal 在多线程环境下的一些问题,尤其是在使用线程池等场景下可能出现的问题。

与普通的 ThreadLocal 不同,TTL 具有以下特点:

(1)线程池透传性:在使用线程池执行任务时,TTL 可以透传 ThreadLocal 的值,确保后续线程能够正确访问前线程设置的 TransmittableThreadLocal 变量值。

(2)线程池隔离性:TTL 在多线程环境下能够确保每个线程都有独立的 TransmittableThreadLocal值,避免了线程池重用线程时可能出现的数据污染问题,比如线程池执行前从父线程继承的变量,不管是执行中变没变,下次执行任务的时候,还是会和父线程保持一致。

(3)资源自动清理:TTL 支持自动清理 TransmittableThreadLocal 值,避免了可能导致内存泄漏的问

题。

(4)兼容性:TTL 兼容原生 ThreadLocal 的语法和用法,可以直接替换原生 ThreadLocal 使用,而无需修改现有代码。

2.2  TransmittableThreadLocal 的使用

TTL 通常用于需要在线程池中执行任务,并且需要在任务之间传递 ThreadLocal 值的场景。

例如,在 Web 应用中,可能需要在异步任务中访问当前用户的会话信息,而使用 TTL 可以确保子线程能够正确访问父线程设置的会话信息。

TransmittableThreadLocal在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能(把任务提交给线程池时的ThreadLocal值传递到任务执行时),解决异步执行时上下文传递的问题。

我们来简单体验一下:

// TTL
private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();
public static void main(String[] args) {
    // 父线程设置变量 1
    tl.set(1);
    new Thread(() -> {
        System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
    }).start();
    new Thread(() -> {
        System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
    }).start();
}

这么一看貌似跟我们的 ITL 没什么区别是吧。我们来看一个线程池的例子:

public class Demo {

    private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();
    
    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            2,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
    );

    public static void main(String[] args) {
        // 父线程设置变量 1
        tl.set(1);        poolExecutor.execute(() -> {
            // 更改当前线程中的值
            tl.set(2);
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });

    }
}

我们看普通情况下,当某个线程改变了 TTL 的值后,下次该线程执行任务的时候,TTL 的值就是改变后的了。这里需要引入一下 TTL 里的线程池,我们再看:

public class Demo {

    private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();

    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            2,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
    );
    private static final Executor ttlExecutor = TtlExecutors.getTtlExecutor(poolExecutor);

    public static void main(String[] args) {
        // 父线程设置变量 1
        tl.set(1);

        ttlExecutor.execute(() -> {
            // 更改当前线程中的值
            tl.set(2);
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });

    }
}

看是不是,1号线程改变了 TTL 的值,下次执行的时候还是和父线程的值一致的。注意下边的这个例子我们的线程池是用TTL里的线程池包装了一层哈(这样才能发挥TTL的效果)。下面我们就来解析一下其内部原理,看看TTL是怎么完成对ITL的优化的。

2.3  源码分析

TTL 本质上是一个InheritableThreadLocal,意味着TTL具备ITL的功能:

// TTL继承关系
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {

具体值是保存在线程内部的inheritableThreadLocals,不涉及线程池时可以当成 InheritableThreadLocal使用。

2.3.1  重要属性 holder

TransmittableThreadLocal重要核心属性holder:

// holder 本质上是一个 InheritableThreadLocal
// 内部的值的类型是 WeakHashMap<TransmittableThreadLocal<Object>, ?>
// 这个 WeakHashMap 被当成一个 Set 使用,这个WeakHashMap的value永远是null, 不会被使用到,key 存的就是 TTL的对象
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
        new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
            }
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
            }
        };

holder的作用:存放的是所有和当前线程产生关联的 TransmittableThreadLocal,通过holder可以找到Java进程中所有的TransmittableThreadLocal(即使用WeakHashMap收集线程中所有的 TransmittableThreadLocal)。

ThreadLocal.ThreadLocalMap和WeakHashMap的区别?

相同点:ThreadLocalMap.Entry和WeakHashMap都继承弱引用,弱引用的reference都指向key :ThreadLocalMap.Entry:

不同点:ThreadLocalMap是一个用移位解决hash冲突的简易Map,而WeakHashMap是一个用链表解决hash冲突的简易Map;

2.3.2  重要方法

下面的方法均属于TTL类:

@Override
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
    return value;
}
@Override
public final void set(T value) {
    if (!disableIgnoreNullValueSemantics && null == value) {
        // may set null to remove value
        remove();
    } else {
        super.set(value);
        addThisToHolder();
    }
}
@Override
public final void remove() {
    // 从holder持有的map对象中移除该 ttl
    removeThisFromHolder();
    super.remove();
}
private void removeThisFromHolder() {
    holder.get().remove(this);
}
private void addThisToHolder() {
    if (!holder.get().containsKey(this)) {
        // 从 holder 中当前线程持有的map 中添加 ttl
        holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
    }
}

TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过上边的例子除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以。

2.3.3  线程池相关重要方法

下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):首先打开包装类,看下execute方法在执行时做了些什么。

// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
public void execute(@NonNull Runnable command) {
    // 这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
    executor.execute(TtlRunnable.get(command));
}
// 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable) {
    return get(runnable, false, false);
}
// 对应上面的get方法
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
    if (null == runnable) return null;
    if (runnable instanceof TtlEnhanced) {
        // 若发现已经是目标类型了(说明已经被包装过了)直接返回
        // avoid redundant decoration, and ensure idempotency
        if (idempotent) return (TtlRunnable) runnable;
        else throw new IllegalStateException("Already TtlRunnable!");
    }
    // 最终的初始化
    return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}
// 对应上面的TtlRunnable方法
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    // 这里就是重点了 这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
    this.capturedRef = new AtomicReference<Object>(capture());
    this.runnable = runnable;
    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
// 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
@Nonnull
public static Object capture() {
    Map<TransmittableThreadLocal<?>, Object> captured = new
    HashMap<TransmittableThreadLocal<?>, Object>();
    for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
        // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
        captured.put(threadLocal, threadLocal.copyValue());
    }
    // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
    return captured; 
}
// 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
private T copyValue() {
    return copy(get());
}
protected T copy(T parentValue) {
    return parentValue;
}

结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef属性,那么在接下来线程执行的时候,也就是run方法里,大概率会将这些捕获到的值赋给子线程的holder赋对应的TTL值,我们继续往下看Rannable包装类里的run方法是怎么实现的:

// run方法属于Rannable的包装类TtlRunnable
@Override
public void run() {
    // 获取由之前捕获到的父线程变量集
    Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    // 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值
    // backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
    Object backup = replay(captured);
    try {
        // 执行异步逻辑
        runnable.run();
    } finally {
        // 这个方法就是用来恢复原有值的
        restore(backup);
    }
}

根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。

下面来具体看这俩方法:

// 下面的方法均属于TTL的静态内部类Transmittable
@Nonnull
public static Object replay(@Nonnull Object captured) {
    @SuppressWarnings("unchecked")
    // 使用此线程异步时捕获到的父线程里的本地变量值
    Map<TransmittableThreadLocal<?>, Object> capturedMap =(Map<TransmittableThreadLocal<?>, Object>) captured; 
    //当前线程原生的本地变量,用于使用完线程后恢复用
    Map<TransmittableThreadLocal<?>, Object> backup = newHashMap<TransmittableThreadLocal<?>, Object>(); 

    //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) {
        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
        TransmittableThreadLocal<?> threadLocal = next.getKey();
        // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用
        backup.put(threadLocal, threadLocal.get()); 
        /**
         * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
         * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
         * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
         * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
         * 意味着很可能会影响到任务里取值的准确性。
         *
         * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
         * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
         * 该tl对象取到值2,不符合预期
         */
        if (!capturedMap.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }
    // 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值)
    setTtlValuesTo(capturedMap);
    // 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述
    doExecuteCallback(true);
    return backup;
}
// 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的
public static void restore(@Nonnull Object backup) {
    @SuppressWarnings("unchecked")
    Map<TransmittableThreadLocal<?>, Object> backupMap =(Map<TransmittableThreadLocal<?>, Object>) backup;
    // call afterExecute callback
    doExecuteCallback(false);
    // 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量)
    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) {
        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
        TransmittableThreadLocal<?> threadLocal = next.getKey();
        /**
        * 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同?
        * 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值)
        */
        if (!backupMap.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }
    // 同样调用这个方法,进行值的恢复
    setTtlValuesTo(backupMap);
}
// 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法
private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
    for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
        @SuppressWarnings("unchecked")
        TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
        threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现
    }
}

好啦,到这里基本上把TTL比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:

 

2.4  应用场景

TransmittableThreadLocal用来实现线程间的参数传递,经典应用场景如下:

(1)分布式跟踪系统 或 全链路压测(即链路打标)

(2)日志收集记录系统上下文

(3)Session级 Cache

(4)应用容器或上层框架跨应用代码给下层 SDK传递信息

2.5  ThreadLocal、InheritableThreaLocal与TransmittableThreadLocal的比较

ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal在Java中都是用于处理线程局部变量的工具,但它们在使用场景和特性上有所不同。

 (1)ThreadLocal

ThreadLocal是Java中一个非常重要的线程技术,它为每个线程提供了它自己的变量副本,使得线程间无法相互访问对方的变量,从而避免了线程间的竞争和数据泄露问题。适用于需要在线程内部存储和获取数据,且不希望与其他线程共享数据的场景。

(2)InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的一个子类,它包含了ThreadLocal的所有功能,并扩展了ThreadLocal的功能。 允许父线程中的InheritableThreadLocal变量的值被子线程继承。当创建一个新的线程时,这个新线程可以访问其父线程中InheritableThreadLocal变量的值。

(3)TransmittableThreadLocal

TransmittableThreadLocal是阿里巴巴开源的一个框架,用于解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题。能够在多线程传递中保持变量的传递性,确保在父线程和子线程之间正确传递ThreadLocal变量。

简单的来说就是:ThreadLocal适用于线程内部的数据存储和访问,确保数据在线程间的隔离。InheritableThreadLocal适用于需要在父线程和子线程间传递数据的场景,实现数据的继承。TransmittableThreadLocal则是为了解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题,实现数据的跨线程传递。

3  小结

好啦,本节就里看到这里哈,有理解不对的地方还请指正哈。

posted @ 2024-05-12 20:18  酷酷-  阅读(1757)  评论(0编辑  收藏  举报