ThreadLocal的進化——TransmittableThreadLocal

NO IMAGE

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

線程池中的共享

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

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 是阿里提供的工具類,其主要解決的就是上面遇到的問題。那麼該如何使用呢?

首先,你需要引入相應的依賴:

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

具體代碼,就拿上文提到的情況,我們用 TransmittableThreadLocal 做一個改造:

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/

公眾號:健程之道

ThreadLocal的進化——TransmittableThreadLocal

ThreadLocal的進化——TransmittableThreadLocal

相關文章

面試官:如果http響應頭中ETag值改變了,是否意味著文件內容一定已經更改

一文解析RabbitMQ最常用的三大模式

通過定時器、時間分片、WebWorker優化長任務

談談Swift中的枚舉內存佈局