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

InheritableThreadLocal简介

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

上一篇文章我们聊到了ThreadLocal的作用机理,但是在文章的末尾,我提到了一个问题,ThreadLocal无法实现异步线程变量的传递。

什么意思呢?以下面的代码为例子:

@SneakyThrows
public Boolean testThreadLocal(String s){
    LOGGER.info("实际传入的值为: " + s);
    DemoContext.setContext(Integer.valueOf(s)); // DemoContext为相应的ThreadLocal对象
    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;
    });
    //打印主线程的值
    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;
}

原本我们期待的结果是,子线程中的值与主线程中的值保持一致,但是实际上,运行代码返回的结果是:

由此可见,ThreadLocal并没有按照所想的那样将相应的ThreadLocal的值传递到相应的异步线程上。

为了实现异步线程变量的传递,InheritableThreadLocal应运而生(以下简称为:ITL)。

我们将上述的代码稍作改动,将demoContext的类型转换成ITL之后再运行一次代码。可以看到结果如下:

ITL虽然传递了主线程的变量信息,但是在特定场景下也会出现问题。例如在上面的代码中,如果我们设置相应的线程池再来请求的话,就会出现问题。源码如下:

@SneakyThrows
public Boolean testThreadLocal(String s){
    ...
        
    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); // 设置了线程池

    ...
}

@Bean(name = "demoExecutor")
public Executor demoExecutor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setTaskDecorator(new GatewayHeaderTaskDecorator());
    threadPoolTaskExecutor.setCorePoolSize(5);
    threadPoolTaskExecutor.setQueueCapacity(0);
    threadPoolTaskExecutor.setKeepAliveSeconds(3600);
    threadPoolTaskExecutor.setMaxPoolSize(1);
    threadPoolTaskExecutor.setThreadNamePrefix("demoExecutor-");
    threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    threadPoolTaskExecutor.initialize();
    return threadPoolTaskExecutor;
}

代码运行起来的结果如下:

可以看到,多次请求后,线程间的变量出现了混乱传递,即实际传入的值,与子线程中拿到的值并不一样。这又是什么原因呢?

底层原理分析

要了解这个问题的原因,我们不得不了解下ITL的工作机制。

其实看起来,但是其实ITL的工作机制很简单,就是在子线程初始化的时候,将父线程的ITL给继承过来。具体来看Thread类中相应的init源码:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        Thread parent = currentThread(); // 先找到他的爸爸

        this.daemon = parent.isDaemon(); //判断是否需要创建的是daemon线程
        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);
        /*关键性代码*/
        // 在这里会判断当前的是否需要inheritThreadLocals
        //如果需要,那么会将当前创建这个线程的InheritableThreadLocals都获取过来,相当于获取了一份父类表的拷贝。
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID(); // 设置ThreadID
    }

这种场景,在不使用线程池的情况是没有问题的。但是如果搭配上了线程池,就会存在问题。这里我们先简单介绍一下线程池的作用机理。

其中最关键的点在于,线程池会复用原有线程,致使部分线程不会经过Init初始化的过程,ITL的值也就没有办法得到更新。最终造成了错误的数据传递。

优劣势分析

优势:

1、通过在线程初始化的时候传递相应的ThreadLocal变量,解决了非线程池下的异步线程的变量传递问题。

劣势:

1、线程池复用线程和ITL底层机制无法兼容,导致了ITL无法结合线程池发挥作用。

总结:

在不依赖于线程池的场景下,ITL是一个很好的实现异步线程传递变量的工具。

然而,在使用线程池的情况下,由于线程不会进行频繁地初始化和销毁等工作,ITL的变量值无法得到更新,因而有可能存在数据错误传递的问题。

参考文献

线程池优点及原理

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