TransmittableThreadLocal解決線程間上下文傳遞煩惱
前言
在一些項目中,經常會遇到需要把當前線程中的上下文傳遞到其他線程中的情況,比如某項目包含國際化操作,在業(yè)務請求進來時需要把對應的國家代碼存儲到當前線程中,以便后續(xù)的業(yè)務邏輯能夠根據國家代碼正確地處理;另外在一些異步化操作中,也要保證異常線程中也能夠正確地獲取到對應的國家代碼。
在上述業(yè)務場景中,我們很自然的就想到了使用ThreadLocal,但是ThreadLocal無法解決父子線程間上下文傳遞的問題,此時InheritableThreadLocal站出來了,它在創(chuàng)建子線程的過程中
拷貝了父親線程中的inheritableThreadLocals數據,在new Thread()代碼中,有一段這樣的代碼:
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
但是在真實的項目當中,異步操作幾乎都是用的線程池來處理,也就意味著線程是復用的,這就導致了不同任務的上下文使用的是同一個線程的上下文,這就會導致程序出現意料不到的BUG。
針對這種情況,我們發(fā)現應該把線程上下文轉變成任務上下文,這樣的話才能避免多個任務共用一個線程上下文,為此我們不得不封裝一下每一個傳入線程池的任務:
class RunnableWrap implements Runnable {
private ThreadLocal threadLocal;
private Object context;
private Runnable task;
public RunnableWrap(ThreadLocal threadLocal, Runnable task) {
this.threadLocal = threadLocal;
this.context = threadLocal.get();
this.task = task;
}
@Override
public void run() {
try {
threadLocal.set(context);
task.run();
} finally {
threadLocal.remove();
}
}
}
但是這樣做確實不是很優(yōu)雅,所以為何不用TransmittableThreadLocal試試呢?
示例
我們來通過一個示例演示一下TransmittableThreadLocal是否能夠在線程池中實現上下文的傳遞,并且滿足任務間上下文的隔離效果:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
// 使用只有一個線程的線程池,測試線程復用是否影響TransmittableThreadLocal的效果
private static final Executor EXECUTOR = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws InterruptedException {
// 設置主線程的上下文為"china"
CONTEXT.set("china");
// 創(chuàng)建第一個任務,通過TtlRunnable.get()包裝;
// 在第一個任務中查看上下文數據,檢查是否拿到正確的上下文;
// 另外再修改掉該上下文,主要測試是否會影響第二個任務的上下文;
Runnable task1 = TtlRunnable.get(() -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "開始");
String countryCode = CONTEXT.get();
System.out.println("第一個任務執(zhí)行結果:" + countryCode);
// 修改該線程中上下文值,檢查是否影響第二個任務
CONTEXT.set("US");
System.out.println(thread.getName() + "結束");
});
// 第二個任務主要測試上下文是否受第一個任務的影響
Runnable task2 = TtlRunnable.get(() -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "開始");
String countryCode = CONTEXT.get();
System.out.println("第二個任務執(zhí)行結果:" + countryCode);
System.out.println(thread.getName() + "結束");
});
// 按順序執(zhí)行兩個任務,全部放到線程池中執(zhí)行
CompletableFuture.runAsync(task1, EXECUTOR1)
.thenRunAsync(task2, EXECUTOR1);
// 檢查主線程上下文是否受影響;
String countryCode = CONTEXT.get();
System.out.println("主線程執(zhí)行結果:" + countryCode);
Thread.sleep(10000);
}
1.我們準備了只有一個線程的線程池,主要測試線程復用的情況;
2.準備了兩個任務,第一個任務檢查是否能夠拿到正確的上下文數據;第二個任務測試是否因為第一個任務修改上下文受到影響;
執(zhí)行結果如下:
pool-1-thread-1開始
第一個任務執(zhí)行結果:china
pool-1-thread-1結束
pool-1-thread-1開始
第二個任務執(zhí)行結果:china
pool-1-thread-1結束
主線程執(zhí)行結果:china
通過上述示例,我們可以得出以下結論:
1.TransmittableThreadLocal可以讓線程池中的上下文保持和父線程一致;
2.TransmittableThreadLocal解決了線程復用導致多任務共享同一個線程上下文的問題;
使用方式
包裝任務
- 通過上述示例,我們學到了最基本的一種使用方式:
TtlRunnable.get(),它可以用來包裝Runnable接口的所有實例; - 同樣的,針對
Callable下的實例,我們可以使用TtlCallable.get()來包裝
包裝線程池
為了我們在使用線程池時,不用每次都使用TtlRunnable或TtlCallable來包裝所有任務,TransmittableThreadLocal還提供了包裝線程池的方法:
TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
通過包裝好的線程池,我們可以修改一下上面的示例代碼:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>();
// 使用只有一個線程的線程池,測試線程復用是否影響TransmittableThreadLocal的效果
private static final Executor EXECUTOR = TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
public static void main(String[] args) throws InterruptedException {
// 設置主線程的上下文為"china"
CONTEXT.set("china");
// 創(chuàng)建第一個任務,通過TtlRunnable.get()包裝;
// 在第一個任務中查看上下文數據,檢查是否拿到正確的上下文;
// 另外再修改掉該上下文,主要測試是否會影響第二個任務的上下文;
Runnable task1 = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "開始");
String countryCode = CONTEXT.get();
System.out.println("第一個任務執(zhí)行結果:" + countryCode);
// 修改該線程中上下文值,檢查是否影響第二個任務
CONTEXT.set("US");
System.out.println(thread.getName() + "結束");
};
// 第二個任務主要測試上下文是否受第一個任務的影響
Runnable task2 = () -> {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "開始");
String countryCode = CONTEXT.get();
System.out.println("第二個任務執(zhí)行結果:" + countryCode);
System.out.println(thread.getName() + "結束");
};
// 按順序執(zhí)行兩個任務,全部放到線程池中執(zhí)行
CompletableFuture.runAsync(task1, EXECUTOR1)
.thenRunAsync(task2, EXECUTOR1);
// 檢查主線程上下文是否受影響;
String countryCode = CONTEXT.get();
System.out.println("主線程執(zhí)行結果:" + countryCode);
Thread.sleep(10000);
}
1.可以看出,我們包裝好線程池后,就不再需要包裝任務了,所有的任務都不需要TtlRunnable.get();
2.從包裝好的線程池中我們可以發(fā)現,返回的實例其實是ExecutorTtlWrapper對象,里面的submit方法、execute()方法上把傳進去Runnable參數使用TtlRunnable.get()做了一層包裝;
小結
本文從業(yè)務角度切入,通過層層遞進的方式從ThreadLocal、InheritableThreadLocal在業(yè)務上的應用及產生的相關問題點,逐步引出TransmittableThreadLocal,通過示例的方式驗證TransmittableThreadLocal符合我們的需求,并且了解了TransmittableThreadLocal針對任務及線程池的使用方式:
1.針對任務Runnable、Callable實例,使用TtlRunnable.get()、TtlCallable.get()包裝;
2.針對線程池,使用TtlExecutors.getTtlExecutor()包裝;
以上就是TransmittableThreadLocal解決線程間上下文傳遞煩惱的詳細內容,更多關于TransmittableThreadLocal線程傳遞的資料請關注腳本之家其它相關文章!
相關文章
SpringBoot ApplicationEvent之事件發(fā)布與監(jiān)聽機制詳解
本文將深入探討SpringBoot的事件機制,介紹其核心概念、實現方法及最佳實踐,幫助開發(fā)者構建更加靈活、可維護的應用架構,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
Apache Calcite進行SQL解析(java代碼實例)
Calcite是一款開源SQL解析工具, 可以將各種SQL語句解析成抽象語法樹AST(Abstract Syntax Tree), 之后通過操作AST就可以把SQL中所要表達的算法與關系體現在具體代碼之中,今天通過代碼實例給大家介紹Apache Calcite進行SQL解析問題,感興趣的朋友一起看看吧2022-01-01
SpringBoot?+?Disruptor實現特快高并發(fā)處理及使用Disruptor高速實現隊列的過程
Disruptor是一個開源的Java框架,它被設計用于在生產者—消費者(producer-consumer problem,簡稱PCP)問題上獲得盡量高的吞吐量(TPS)和盡量低的延遲,這篇文章主要介紹了SpringBoot?+?Disruptor?實現特快高并發(fā)處理,使用Disruptor高速實現隊列,需要的朋友可以參考下2023-11-11
Java使用ProcessBuilder?API優(yōu)化流程
Java?的?Process?API?為開發(fā)者提供了執(zhí)行操作系統命令的強大功能,這篇文章將詳細介紹如何使用?ProcessBuilder?API?來方便的操作系統命令,需要的可以收藏一下2023-06-06
SpringBoot中使用Flyway進行數據庫遷移的詳細流程
本文介紹了如何在Spring Boot項目中使用Flyway進行數據庫遷移,Flyway通過SQL腳本管理數據庫變更,支持自動執(zhí)行和版本控制,避免了手動執(zhí)行SQL腳本的錯誤和維護困難,需要的朋友可以參考下2025-02-02

