多线程热知识(三):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());
    }

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

可以明显看到,即使在使用线程池的情况下,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中的内容进行任意的修改,同时也不会影响到原线程的逻辑。

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

因此采用了截取快照加重放的机制。除了上述提到的设计,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官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容