多线程热知识(三):TransmittableThreadLocal,异步线程变量传递最优解

TTL简介

多线程热知识(一):ThreadLocal简介及底层原理

多线程热知识(二):异步线程变量传递必知必会---InheritableThreadLocal及底层原理分析

之前的文章我们介绍了ThreadLocal(TL)及InheritableThreadLocal(ITL)各自的作用机制及其优缺点。在ITL的文章最后,我们了解到ITL在使用线程池的情况下,由于其作用机制依赖于线程的Init方法,因此,没有办法很好的解决异步现成传递的问题。

那么有没有能够在线程池下也解决这个问题的利器呢?

肯定的回答说,有!

就是我们下来需要介绍的TransmittableThreadLocal。

以ITL中我们介绍的例子做相应的改造,其中,我们将DemoContext的类型修改成TransmittableThreadLocal,同时将线程池采用ttl提供的方式进行包装,从而实现相应的改造。

    @SneakyThrows
    public Boolean testThreadLocal(String s){
        LOGGER.info("实际传入的值为: " + s);
        DemoContext.setContext(Integer.valueOf(s)); // 同时DemoContext也需要设成TransmittableThreadLocal
        CompletableFuture<Throwable> subThread = CompletableFuture.supplyAsync(()->{
            try{
                LOGGER.info(String.format("子线程id=%s,contextStr为:%s"
                                          ,Thread.currentThread().getId(),DemoContext.getContext()));
            }catch (Throwable throwable){
                return throwable;
            }
            return null;
        },demoExecutor); // 这里demoExecutor需要采用ttl提供的线程池类
        LOGGER.info(String.format("主线程id=%s,contextStr为:%s"
                                  ,Thread.currentThread().getId(),DemoContext.getContext()));
        Throwable throwable = subThread.get();
        if (throwable!=null){
            throw throwable;
        }
        DemoContext.clearContext();
        return true;
    }
	
	...
    
    @Bean(name = "demoExecutor")
    public Executor demoExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setTaskDecorator(new GatewayHeaderTaskDecorator());
        threadPoolTaskExecutor.setCorePoolSize(5);
        threadPoolTaskExecutor.setQueueCapacity(0);
        threadPoolTaskExecutor.setKeepAliveSeconds(3);
        threadPoolTaskExecutor.setMaxPoolSize(50);
        threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(false);
        threadPoolTaskExecutor.initialize();
        //采用ttl对相应的线程池进行包装
        return TtlExecutors.getTtlExecutor(threadPoolTaskExecutor.getThreadPoolExecutor());
    }
复制代码

然后分别多次请求,可以看到相应的结果如下:

image-20220223201343467.png

可以明显看到,即使在使用线程池的情况下,TransmittableThreadLocal也实现了正常的值传递。

更多的使用方法,如单纯创建线程不采用线程池的方式等,可以参考一下TTL的官方文档

底层原理

那么TTL底层到底是如何实现的呢?让我们从源码来一步步看起~

首先,很【明显】地,TTL实现能力的机密主要在于它对线程池的包装,这里我们就先分析他是如何包装线程池的。首先查看getTtlExecutor的源码内容:

@Nullable
public static Executor getTtlExecutor(@Nullable Executor executor) {
    if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) {
        return executor;
    }
    return new ExecutorTtlWrapper(executor, true);
}

复制代码

可以看到,这里只是简单的返回了一个包装的线程池,那么我们就必须再深入去看看。找到对应的线程池类ExecutorTtlWrapper,小瞅了一眼线程池的excute方法:

class ExecutorTtlWrapper implements Executor, TtlWrapper<Executor>, TtlEnhanced {
    ...
        
	@Override
    public void execute(@NonNull Runnable command) {
        executor.execute(TtlRunnable.get(command, false, idempotent));
    }
    
    ...
}
复制代码

嗯?这个**TtlRunnable.get()**很明显是对我们需要执行的Runnable任务做了相应的封装。跟进源代码:

@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类,可以发现咱们的关键方法run(),和关键的构造方法。其中尤其值得注意的就是构造方法中的**capture()**方法。

public final class TtlRunnable {
    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.capturedRef = new AtomicReference<Object>(capture()); // 关键代码
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
}
复制代码

简要介绍**capture()**方法的话,这里的操作其实就是将父线程的TTL变量集合生成相应的快照记录,并随着任务创建包装的时候,保存到生成的子线程中,由此实现了异步线程在线程池下的变量传递。

@NonNull
public static Object capture() {
    //生成快照
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

private static class Snapshot {
    final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
    final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;

    private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
        this.ttl2Value = ttl2Value;
        this.threadLocal2Value = threadLocal2Value;
    }
}
复制代码

到此的话,整个异步变量的实现原理就已经完成了,想起来很朴素,但其实又在情理之中。但是除此之外,ttlRunable的run方法中还有一个很精妙的点,值得我跟大家再介绍介绍~

    @Override
    public void run() {
        final Object captured = capturedRef.get(); //获取所有的ttl及tl快照内容
        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); // 随后恢复线程的任务至初始的备份状态
        }
    }
    
复制代码

起初,我对这里感到十分的疑惑。为什么要做重放的操作鸭?

直到我看了原作者的评论回复,加上自己的一些理解和思考,得出了下述的结论:

原因主要是考虑在线程池最大线程数确定,且线程池的拒绝策略采用的是CallerRunsPolicy的情况下,两次执行的程序可能都是业务线程自己本身,如果不采用重放机制,中途对TTL的内容如果进行了修改,那么就会存在问题。

如下图是正常情况下,父子线程的执行方式,这个时候,由于父子线程的数据是隔离开的,那么此时子线程可以对TTL中的内容进行任意的修改,同时也不会影响到原线程的逻辑。

image-20220223205406304.png

但是如果在业务高峰期,线程池最大线程数量及阻塞队列都占慢了,而且采用了callerRunsPolicy的拒绝策略,那么这个时候任务的执行图就可能如下所示。

image-20220223210645154.png

因此采用了截取快照加重放的机制。除了上述提到的设计,TTL中还有很多精妙的设计,比如保存每个线程TTL数据的Holder变量。这里限于篇幅的原因,就只是简单的抛砖引玉啦~

    // Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        the value of WeakHashMap is *always* null, and never used.
    //    2.2 WeakHashMap support *null* value.
	//大致推断,holder是保存每个线程TTL的地方
    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
            new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                    //创建一个初始为null的set集合
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
                }

                @Override
                protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                    //拷贝我爸的TTL内容
                    return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
                }
            };
复制代码

总结

总的来说,TTL通过将异步线程变量的传递时机由线程初始创建的时候,后移到了线程任务执行的时候。这样一来确保了线程变量即使在使用了线程池的时候也能够相应的传递下去。

另外,采用了线程变量快照及重放的机制,避免了在高并发情况下可能出现的业务数据紊乱的问题,是很精妙的设计。

如果你看到了这里,不妨给我点个赞、点个收藏,要是还能关注一下下就更好啦~

创作不易,感谢支持~

参考文献

ttl官方文档

Guess you like

Origin juejin.im/post/7068222037226946596