TransmittableThreadLocal(ttl)实现异步运行时的上下文传递问题

109 阅读5分钟

[toc]

某些业务场景下,我们需要让一些上下文信息在多个系统之间进行扭转。总所周知,ThreadLocal就是用于解决线程内局部变量安全问题的。但是其不具备传递性,一旦业务内进行线程切换,那么上下文信息就会丢失。曾经,我也尝试着去解决这个问题:方案很简单,每当线程切换的时候就将父线程的上下文信息放到子线程中。但是这个方案逻辑层面解决的,并不适合真正的业务场景。怕是不会有哪个项目使用多线程还是即用即销吧,线程池才是真爱呀~。用了线程池就没有什么父子线程的关系了(从操作系统层面来看,也从来没有什么父子线程)。

InheritableThreadLocal(可传递的ThreadLocal)

我之前的demo就和InheritableThreadLocal不谋而合了,万分有幸能和jdk支持方案想到一块。哈哈哈哈哈~

要讲清楚InheritableThreadLocal​​的原理,就不得不先提一下线程之间的关系。在Java中,我们经常能听到父线程、子线程的概念,严格来说,Java中不存在实质上的父子关系,所谓的父线程的概念只是一种逻辑称呼。没有一个方法可以获取一个线程的父线程吧,也没有一个方法可以获取一个线程的子线程吧。子线程的消亡和父线程的消亡没有任何关系,不会因为谁先结束而导致自身退出吧(操作系统中就是如此了)。

那么为什么还会有父线程这种称呼呢?父线程的称呼来自于线程的构造方法,口说无凭,我们看源码:

// 删减后部分源码
private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        // 用当前线程作为父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        this.tid = nextThreadID();
    }

从上面的代码就可以看出,手动创建线程的过程中会将当前线程的一些配置信息直接拷贝到新的线程中进行一些必要的赋值和初始化,并且把当前线程取名为parent​​,所以说所谓父线程仅仅是逻辑称呼罢了。大可不必不必理会,哈哈哈哈~

说回InheritableThreadLocal​​吧;值得一说的是,jdk​​其实早就考虑到这种场景需求了。所以原生就提供了InheritableThreadLocal​​用于父子线程传递线程变量。原理就是跟ThreadLocal​​一样,存到当前线程中,一旦当前线程要创建新的线程了就把自身的线程变量拷贝过去(jdk你多冒昧呀,哈哈哈) 。但是这种方式根本不适用,线程只会在被创建和使用一次时是有效的,线程一旦池化就是然并卵。使用线程池场景下,由于线程被复用,初始化一次后续使用就不会走这个ThreadLocal​​的传递过程。别说传递了,你会发现第一个线程使用后,线程中的变量比钻石还金贵,一颗永流传~。

TransmittableThreadLocal(可传递的ThreadLocal,以下统称TTL)

天空一声巨响,主角闪亮登场~,TTL​​是阿里开源用于真正解决异步执行时上下文传递问题的组件,在InheritableThreadLocal​​基础上,实现了线程复用场景下的线程变量传递功能。

使用方式:

  1. 直接使用

那就跟ThreadLocal​一样了,使用TTL​存入变量,子线程直接用。亲父子直接继承,哈哈哈哈~

  1. 提交线程池使用
  1. 使用增强的Runnable​或者Callable​就是TtlRunable​和TtlCallbalbe​,提交线程池会在run()内取出变量;
  2. 使用增强的线程池:本质上也是装饰Runable​,多得不说了,说一万句不如自己去看源码。看看我这不负责的样子。哈哈哈哈哈

核心原理(技术人的重头戏来了)

  1. 如何设置线程变量

当调用TransmittableThreadLocal.set()​设置变量的时候会调用super.set()​设置当前线程变量,还会执行addThisToHolder()​方法,在其内部维护了一个静态holder变量,保存当前线程下的所有TTL线程变量:

private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>();

    @Override
    protected WeakHashMap<TransmittableThreadLocal<Object>, ? initialValve() {
        return new WeakHashMap<TransmittableThreadLocal<object>, Object>();
    }

    @Override
    protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ? parentValue) {
        return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
    }

    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, nuLl); // WeakHashMap supports null value.
        }
    }

2. 如何构建TtlRunnable对象

构建TtlRunnable​ 时,会保存原Runnable​ 对象引用,用于后续run()方法中业务代码的执行,另外还会调用TransmittableThreadLocal.Transmitter.capture()​方法,缓存当前主线程变量;实际上是对第一步在holder中保存的 ThreadLocal​对象进行那边离,保存其变量值。就实现了复制一份到当前的对象中。

    public static Object capture() {
        return new Snapshot(captureTtlValves(), captureThreadLocalValves());
    }

    // 复制所有TTL线程变量
    private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
        HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<object>, Object>()
        ;
        for (TransmittableThreadLocal<object> threadLocal = holder.get().keySet()) {
            ttl2Value.put(threadLocal, threadLocal.copyValue());
        }
        return ttl2Valve;
    }

    // 复制所有线程变量(用于支持原生ThreadLocal的传递)
    private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
        final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<object>, Object>();
        for (Map.Entry<ThreadLocal<Object>, TtlCopier<object>> entry = threadLocalHolder.entrySet()){
            final ThreadLocal<Object> threadLocal entry.getKey();
            final TtlCopier<object> copier entry.getValue();
            threadLocal2Valve.put(threadLocal, copier.copy(threadLocal.get()));
        }
        return threadLocal2Value;
    }

3. 子线程中读取变量

当TtlRunnable​对象被提交到线程池执行时,调用TtlRunnable.run()​就会从Runnable​对象中取出缓存的线程变量capture

public void run() {
        Object captured = this.capturedRef.get();
        if (captured != null && (!this.releaseTtlValueReferenceAfterRun || this.capturedRef.compareAndSet(captured, (Object)null))) {
            Object backup = Transmitter.replay(captured);

            try {
                this.runnable.run();
            } finally {
                Transmitter.restore(backup);
            }

        } else {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
    }

处理工序:

3.1 TransmittableThreadLocal.Transmitter.replay()

public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull java.util.HashMap<TransmittableThreadLocal<Object>, Object> captured) {
        HashMap<TransmittableThreadLocal<Object>, Object> backup = TransmittableThreadLocal.newHashMap(((WeakHashMap)TransmittableThreadLocal.holder.get()).size());
        Iterator<TransmittableThreadLocal<Object>> iterator = ((WeakHashMap)TransmittableThreadLocal.holder.get()).keySet().iterator();

        while(iterator.hasNext()) {
            TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal)iterator.next();
            backup.put(threadLocal, threadLocal.get());
            if (!captured.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        TransmittableThreadLocal.Transmitter.setTtlValuesTo(captured);
        TransmittableThreadLocal.doExecuteCallback(true);
        return backup;
 }

3.2 执行run()方法,读取变量值

3.3 ​TransmittableThreadLocal.Transmitter.restore()

public void restore(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {
        TransmittableThreadLocal.doExecuteCallback(false);
        Iterator<TransmittableThreadLocal<Object>> iterator = ((WeakHashMap) TransmittableThreadLocal.holder.get()).keySet().iterator();

        while (iterator.hasNext()) {
            TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal) iterator.next();
            if (!backup.containsKey(threadLocal)) {
                iterator.remove();
                threadLocal.superRemove();
            }
        }

        TransmittableThreadLocal.Transmitter.setTtlValuesTo(backup);
    }

到这有点累了,看源码还是很痛苦的~给个官方的时序图帮助理解一下吧:

image-20240718162720-ohe874m.png

总结:从实现线程变量传递的原理上来看,TTL做的实际上就是将原本与Thread绑定的线程变量,缓存一份到TtlRunnable对象中,在执行子线程任务前,将对象中缓存的变量值设置到子线程的ThreadLocal中以供run()方法的代码使用,然后执行完后,又恢复现场,保证不会对复用线程产生影响。

浅浅就到这吧,不装了,下班~