多線程熱知識(二):異步線程變量傳遞必知必會---InheritableThreadLocal及底層原理分析

InheritableThreadLocal簡介

多線程熱知識(一):ThreadLocal簡介及底層原理

上一篇文章我們聊到了ThreadLocal的作用機(jī)理,但是在文章的末尾,我提到了一個問題,ThreadLocal無法實(shí)現(xiàn)異步線程變量的傳遞。

什么意思呢?以下面的代碼為例子:

@SneakyThrows
public Boolean testThreadLocal(String s){
    LOGGER.info("實(shí)際傳入的值為: " + s);
    DemoContext.setContext(Integer.valueOf(s)); // DemoContext為相應(yīng)的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;
}

原本我們期待的結(jié)果是,子線程中的值與主線程中的值保持一致,但是實(shí)際上,運(yùn)行代碼返回的結(jié)果是:

由此可見,ThreadLocal并沒有按照所想的那樣將相應(yīng)的ThreadLocal的值傳遞到相應(yīng)的異步線程上。

為了實(shí)現(xiàn)異步線程變量的傳遞,InheritableThreadLocal應(yīng)運(yùn)而生(以下簡稱為:ITL)。

我們將上述的代碼稍作改動,將demoContext的類型轉(zhuǎn)換成ITL之后再運(yùn)行一次代碼??梢钥吹浇Y(jié)果如下:

ITL雖然傳遞了主線程的變量信息,但是在特定場景下也會出現(xiàn)問題。例如在上面的代碼中,如果我們設(shè)置相應(yīng)的線程池再來請求的話,就會出現(xiàn)問題。源碼如下:

@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); // 設(shè)置了線程池

    ...
}

@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;
}

代碼運(yùn)行起來的結(jié)果如下:

可以看到,多次請求后,線程間的變量出現(xiàn)了混亂傳遞,即實(shí)際傳入的值,與子線程中拿到的值并不一樣。這又是什么原因呢?

底層原理分析

要了解這個問題的原因,我們不得不了解下ITL的工作機(jī)制。

其實(shí)看起來,但是其實(shí)ITL的工作機(jī)制很簡單,就是在子線程初始化的時候,將父線程的ITL給繼承過來。具體來看Thread類中相應(yīng)的init源碼:

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

        this.daemon = parent.isDaemon(); //判斷是否需要創(chuàng)建的是daemon線程
        this.priority = parent.getPriority(); // 保持跟他爸一樣的優(yōu)先級
        if (security == null || isCCLOverridden(parent.getClass())) // 獲取相應(yīng)的加載器
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        /*關(guān)鍵性代碼*/
        // 在這里會判斷當(dāng)前的是否需要inheritThreadLocals
        //如果需要,那么會將當(dāng)前創(chuàng)建這個線程的InheritableThreadLocals都獲取過來,相當(dāng)于獲取了一份父類表的拷貝。
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        tid = nextThreadID(); // 設(shè)置ThreadID
    }

這種場景,在不使用線程池的情況是沒有問題的。但是如果搭配上了線程池,就會存在問題。這里我們先簡單介紹一下線程池的作用機(jī)理。

其中最關(guān)鍵的點(diǎn)在于,線程池會復(fù)用原有線程,致使部分線程不會經(jīng)過Init初始化的過程,ITL的值也就沒有辦法得到更新。最終造成了錯誤的數(shù)據(jù)傳遞。

優(yōu)劣勢分析

優(yōu)勢:

1、通過在線程初始化的時候傳遞相應(yīng)的ThreadLocal變量,解決了非線程池下的異步線程的變量傳遞問題。

劣勢:

1、線程池復(fù)用線程和ITL底層機(jī)制無法兼容,導(dǎo)致了ITL無法結(jié)合線程池發(fā)揮作用。

總結(jié):

在不依賴于線程池的場景下,ITL是一個很好的實(shí)現(xiàn)異步線程傳遞變量的工具。

然而,在使用線程池的情況下,由于線程不會進(jìn)行頻繁地初始化和銷毀等工作,ITL的變量值無法得到更新,因而有可能存在數(shù)據(jù)錯誤傳遞的問題。

參考文獻(xiàn)

線程池優(yōu)點(diǎn)及原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容