ThreadLocal的進化——TransmittableThreadLocal

上一篇文章中,咱們談到了 InheritableThreadLocal,它解決了 ThreadLocal 針對父子線程沒法共享上下文的問題。但咱們可能據說過阿里的開源產品TransmittableThreadLocal,那麼它又是作什麼的呢? git

線程池中的共享

咱們在多線程中,不多會直接 new 一個線程,更多的多是利用線程池處理任務,那麼利用 InheritableThreadLocal 能夠將生成任務線程的上下文傳遞給執行任務的線程嗎?廢話很少說,直接上代碼測試一下:github

public class InheritableThreadLocalContext {

    private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();

    static class Context {

        String name;

        int value;
    }

    public static void main(String[] args) {
        // 固定線程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任務的線程對context進行賦值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        InheritableThreadLocalContext.context.set(contextMain);
                        // 提交任務
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            executorService.execute(() -> {
                                // 執行任務的子線程
                                Context contextChild = InheritableThreadLocalContext.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            });
                        }

                    }
            ).start();
        }
    }
}複製代碼

咱們但願的結果是,子線程輸出的內容可以和父線程對應上。然而,實際的結果卻出乎所料,我將結果整理一下:多線程

Thread1 produce task 21
// 省略8行
Thread1 produce task 30

Thread2 produce task 41
// 省略8行
Thread2 produce task 50
pool-1-thread-1 execute task, name : Thread2 name value : 40
// 省略47行
pool-1-thread-1 execute task, name : Thread2 name value : 40

Thread3 produce task 61
// 省略8行
Thread3 produce task 70

Thread4 produce task 81
// 省略8行
Thread4 produce task 90

Thread5 produce task 101
// 省略8行
Thread5 produce task 110

Thread6 produce task 121
// 省略8行
Thread6 produce task 130

Thread7 produce task 141
// 省略8行
Thread7 produce task 150
pool-1-thread-2 execute task, name : Thread7 name value : 140
// 省略6行
pool-1-thread-2 execute task, name : Thread7 name value : 140

Thread8 produce task 161
// 省略8行
Thread8 produce task 170

Thread9 produce task 181
// 省略8行
Thread9 produce task 190
pool-1-thread-4 execute task, name : Thread9 name value : 180
pool-1-thread-4 execute task, name : Thread9 name value : 180

Thread10 produce task 201
// 省略8行
Thread10 produce task 210
pool-1-thread-3 execute task, name : Thread10 name value : 200
// 省略39行
pool-1-thread-3 execute task, name : Thread10 name value : 200複製代碼

雖然生產總數和消費總數都是100,可是明顯有的消費多了,有的消費少了。合理推測一下,應該是在主線程放進任務後,子線程才生成。爲了驗證這個猜測,將線程池用 ThreadPoolExecutor 生成,並在用子線程生成任務以前,先賦值 context 並開啓全部線程:異步

public static void main(String[] args) {
        // 固定線程池
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                4,
                4,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>() );
        // 在main線程中賦值
        Context context = new Context();
        context.name = "Thread0 name";
        context.value = 0;
        InheritableThreadLocalContext.context.set(context);
        // 開啓全部線程
        executorService.prestartAllCoreThreads();

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任務的線程對context進行賦值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        InheritableThreadLocalContext.context.set(contextMain);
                        // 提交任務
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            executorService.execute(() -> {
                                // 執行任務的子線程
                                Context contextChild = InheritableThreadLocalContext.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            });
                        }

                    }
            ).start();
        }
    }複製代碼

結果不出所料,執行任務的線程輸出的,都是最外面主線程設置的值。工具

那麼咱們該如何才能達到最初想要的效果呢?就是利用線程池執行任務時,如何可以讓執行者線程可以獲取調用者線程的 context 呢?性能

使用 TransmittableThreadLocal 解決

上面的問題主要是由於執行任務的線程是被線程池管理,能夠被複用(能夠稱爲池化複用)。那複用了以後,若是仍是依賴於父線程的 context,天然是有問題的,由於咱們想要的效果是執行線程獲取調用線程的 context,這時候就是TransmittableThreadLocal出場了。測試

TransmittableThreadLocal 是阿里提供的工具類,其主要解決的就是上面遇到的問題。那麼該如何使用呢?spa

首先,你須要引入相應的依賴:線程

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.0</version>
</dependency>複製代碼

具體代碼,就拿上文提到的狀況,咱們用 TransmittableThreadLocal 作一個改造:rest

public class TransmittableThreadLocalTest {
    private static TransmittableThreadLocal<Context> context = new TransmittableThreadLocal<>();

    static class Context {

        String name;

        int value;
    }

    public static void main(String[] args) {
        // 固定線程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任務的線程對context進行賦值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        TransmittableThreadLocalTest.context.set(contextMain);
                        // 提交任務
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            Runnable task = () -> {
                                // 執行任務的子線程
                                Context contextChild = TransmittableThreadLocalTest.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            };
                            // 額外的處理,生成修飾了的對象ttlRunnable
                            Runnable ttlRunnable = TtlRunnable.get(task);
                            executorService.execute(ttlRunnable);
                        }

                    }
            ).start();
        }
    }
}複製代碼

此時再次運行,就會發現執行線程運行時的輸出內容是徹底能夠和調用線程對應上的了。固然了,我這種方式是修改了 Runnable 的寫法,阿里也提供了線程池的寫法,簡單以下:

public static void main(String[] args) {
        // 固定線程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 額外的處理,生成修飾了的對象executorService
        executorService = TtlExecutors.getTtlExecutorService(executorService);
        ExecutorService finalExecutorService = executorService;

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任務的線程對context進行賦值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        TransmittableThreadLocalTest.context.set(contextMain);
                        // 提交任務
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            Runnable task = () -> {
                                // 執行任務的子線程
                                Context contextChild = TransmittableThreadLocalTest.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            };
                            finalExecutorService.execute(task);
                        }

                    }
            ).start();
        }
    }複製代碼

其實還有更加簡單的寫法,具體能夠參考其github:https://github.com/alibaba/transmittable-thread-local

總結

其實兩篇 ThreadLocal 升級文章的出現,都是由於週三聽了一個部門關於 TTL 的分享會,也是介紹了 TransmittableThreadLocal,但由於攜程商旅面臨國際化的改動,當前的語種信息確定是存儲在線程的 context 中最方便,但涉及到線程傳遞的問題(由於會調用異步接口等等),因此天然就須要考慮這個了。性能方面的話,他們有作過測試,但我也只是一個聽者,並無具體使用過,你們也能夠一塊兒交流。

有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。

death00.github.io/

公衆號:健程之道

相關文章
相關標籤/搜索