ThreadLocal垮線程池傳遞數據解決方案:TransmittableThreadLocal【享學Java】

企業不是慈善機構:創造利潤是你存在的核心價值。

前言

上篇文章 瞭解到了,ThreadLocal它並不能解決線程安全問題,它旨在用於傳遞數據。但是它能成功傳遞數據比如有個大前提:放數據和取數據的操作必須是處於相同線程

即使JDK擴展出了一個子類:InheritableThreadLocal,它能夠支持跨線程傳遞數據,但也僅限於父線程給子線程來傳遞數據。倘若兩個線程間真的八竿子打不着,比如分別位於兩個線程池內的線程,它們之間要傳遞數據該腫麼辦呢?這就是跨線程池之間的數據傳遞範疇,是本文將要講解的主要內容。


正文

在實際生產中,線程一般不可能孤立的獨立去運行,而是交給線程池去調度處理。所以實際上幾乎沒有純正的父子線程的關係存在,而若有這種需求大多是線程池與線程池之間的線程聯繫。


InheritableThreadLocal的侷限性

上篇文章 介紹了ThreadLocal的侷限性,可以使用更強的子類InheritableThreadLocal予以解決。那麼這裏看看如下示例:

public class TestThreadLocal {

    private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
    private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();

    @Test
    public void fun1() throws InterruptedException {
        THREAD_LOCAL.set(new Person());


        THREAD_POOL.execute(() -> getAndPrintData());
        TimeUnit.SECONDS.sleep(2);
        Person newPerson = new Person();
        newPerson.setAge(100);
        THREAD_LOCAL.set(newPerson); // 給線程重新綁定值


        THREAD_POOL.execute(() -> getAndPrintData());
        TimeUnit.SECONDS.sleep(2);
    }


    private void setData(Person person) {
        System.out.println("set數據,線程名:" + Thread.currentThread().getName());
        THREAD_LOCAL.set(person);
    }

    private Person getAndPrintData() {
        Person person = THREAD_LOCAL.get();
        System.out.println("get數據,線程名:" + Thread.currentThread().getName() + ",數據爲:" + person);
        return person;
    }

    @Setter
    @ToString
    private static class Person {
        private Integer age = 18;
    }
}

運行程序,控制檯打印:

get數據,線程名:pool-1-thread-1,數據爲:TestThreadLocal.Person(age=18)
get數據,線程名:pool-1-thread-1,數據爲:TestThreadLocal.Person(age=18)

重新綁定竟然“未生效”?在原基礎上什麼都不動,僅僅只改變線程池的大小:

private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(2);

再次運行程序,控制檯打印:

get數據,線程名:pool-1-thread-1,數據爲:TestThreadLocal.Person(age=18)
get數據,線程名:pool-1-thread-2,數據爲:TestThreadLocal.Person(age=100)

這個結果能接受且符合預期。可以看到線程名是不一樣的,所以第二個線程獲取到了最新綁定的結果。因此可以大膽猜測:線程在init初始化的時候,纔會去同步一份最新數據過來

對於這兩個示例的結果可做如下解釋:

  • 示例1的線程池大小是1,所以第二個線程執行時複用的是上個線程(你看線程名稱都一樣),所以就不會再經歷init初始化階段,所以得到的綁定數據還是舊數據
  • 示例2的線程池大小是2,所以第二個線程執行時會繼續初始化一條新的線程來執行它,會觸發到init過程,所以它獲取到的是最新綁定的數據。

小提示:線程池內線程數量若還沒達到coreSize大小的話,每次新任務都會啓用新的線程來執行的(不管是否有空閒線程與否)


Thread#init方法探究

爲了理解後面方案的實現,非常有必要對線程初始化方法Thread#init理解一番。

Thread#init:

	// inheritThreadLocals是否繼承線程的本地變量們(默認是true)
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
		Thread parent = currentThread();
		...
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		...
        /* Set thread ID */ // 給線程一個自增的id
        tid = nextThreadID();
	}

子線程是通過在父線程中通過調用new Thread()方法來創建子線程Thread#init方法在Thread的構造方法中被調用。

從摘錄出來的源碼出能得到如下重點:

  1. 當前線程作爲新創建線程(子線程)的父線程
  2. 如果父線程綁定了變量(inheritableThreadLocals != null)並且允許繼承(inheritThreadLocals = true),那麼就會把父線程綁定的變量們 拷貝一份到子線程裏
    1. 拷貝的原理類似於Map複製,只不過其在Hash衝突時,不是使用鏈表結構,而是直接在數組中找下一個爲null的槽位放裏面

說明:這裏的拷貝是淺拷貝:引用傳遞而已。如果想要深度拷貝,需要自行復寫ThreadLocal#childValue()方法(比如你可以繼承InheritableThreadLocal並重寫childValue方法)

那麼爲何ThreadLocal不具備繼承性,而InheritableThreadLocal可以呢?有了上面的知識儲備,現在一探其源碼便知:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

	// 現在知道爲何是淺拷貝了吧~~~~~~
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    // 只要inheritableThreadLocals不爲null了,那可不就完成子線程可以繼承父的了嗎
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

}

源碼不會騙人,一切都透露得明明白白的了吧。


InheritableThreadLocal支持子線程訪問父線程中本地變量的原理是:創建子線程時將父線程中的本地變量值拷貝了一份到自己這來,拷貝的時機是子線程創建時

然後在實際開發中,多線程就離不開線程池的使用,因爲線程池能夠複用線程,減少線程的頻繁創建與銷燬。倘若合格時候使用InheritableThreadLocal來傳遞數據,那麼線程池中的線程拷貝的數據始終來自於第一個提交任務的外部線程,這樣非常容易造成線程本地變量混亂,這種錯誤是致命的,比如示例1就是這種例子~

那麼,這種問題怎麼破?JDK並沒有提供源生的支持,這時候就得藉助阿里巴巴開源的TTL(transmittable-thread-local):TransmittableThreadLocal


TransmittableThreadLocal

TTL是阿里巴巴開源的專門解決InheritableThreadLocal的侷限性,實現線程本地變量在線程池的執行過程中,能正常的訪問父線程設置的線程變量。

TransmittableThreadLocal簡稱TTL,InheritableThreadLocal簡稱ITL

它的官網是:https://github.com/alibaba/transmittable-thread-local
功能介紹我已截圖至此:

在這裏插入圖片描述
GAV:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

那麼使用它就能解決如上示例的問題嗎?正所謂試驗是檢驗真理的唯一標準,來一把:

針對示例1,僅僅做出如下改動(其它均不變):

// 實現類使用TTL的實現
private static final ThreadLocal<Person> THREAD_LOCAL = new TransmittableThreadLocal<>();
// 線程池使用TTL包裝一把
private static final ExecutorService THREAD_POOL = TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());

再次運行程序,控制檯打印:

get數據,線程名:pool-1-thread-1,數據爲:TestThreadLocal.Person(age=18)
get數據,線程名:pool-1-thread-1,數據爲:TestThreadLocal.Person(age=100)

bingo!看線程名仍舊還是同一個線程(因爲線程池大小爲1嘛),但是結果已經是最新的了,這纔是合理的嘛,不禁想感嘆一句:太它xxxxx了!

說明:這裏線程池必須使用TtlExecutors處理一下,而且得使用TransmittableThreadLocal作爲數據傳遞的實現,缺一不可哦~


如何實現?

TransmittableThreadLocal繼承於InheritableThreadLocal,並擁有了 InheritableThreadLocal對子線程傳遞上下文的特性,只需解決線程池上下文傳遞問題。它使用TtlRunnable包裝了任務的運行,被包裝的run方法執行異步任務之前,會使用replay進行設置父線程裏的本地變量給當前子線程,任務執行完畢,會調用restore恢復該子線程原生的本地變量,當然重點還是稍顯複雜的上下文管理部分。

本文並不涉及到它詳細的原理,建議有興趣者可以上它官網看看(不算很複雜),全中文的也好理解,並且還附有其執行時序圖。

說明:它還支持javaagent完全零侵入方式接入,可以說是非常強大和好用的一個基礎工具,值得使用明白,對中間件團隊能提供良好的支持。


使用場景

官方流出了其四大使用場景:

  1. 分佈式跟蹤系統(鏈路追蹤)
  2. 日誌收集記錄系統上下文(MDC)
  3. Session級Cache
  4. 應用容器或上層框架跨應用代碼給下層SDK傳遞信息

其中場景1和場景2在全鏈路壓測平臺打造的時候都會觸及到,所以基於TTL來解決這些問題不失外一個非常好的選擇。


總結

ThreadLocal的一步步的進化,最終來到了TransmittableThreadLocal,它能夠滿足我們對線程間數據傳遞的幾乎一切遐想,這對我們做類似於全鏈路壓測這種平臺的時候非常有幫助,期待成效。

分隔線

聲明

原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲。你也可【左邊掃碼/或加wx:fsx641385712】邀請你加入我的 Java高工、架構師 系列羣大家庭學習和交流。
往期精選

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章