TransmittableThreadLocal解決線程間上下文傳遞煩惱
前言
在一些項(xiàng)目中,經(jīng)常會(huì)遇到需要把當(dāng)前線程中的上下文傳遞到其他線程中的情況,比如某項(xiàng)目包含國際化操作,在業(yè)務(wù)請(qǐng)求進(jìn)來時(shí)需要把對(duì)應(yīng)的國家代碼存儲(chǔ)到當(dāng)前線程中,以便后續(xù)的業(yè)務(wù)邏輯能夠根據(jù)國家代碼正確地處理;另外在一些異步化操作中,也要保證異常線程中也能夠正確地獲取到對(duì)應(yīng)的國家代碼。
在上述業(yè)務(wù)場景中,我們很自然的就想到了使用ThreadLocal
,但是ThreadLocal
無法解決父子線程間上下文傳遞的問題,此時(shí)InheritableThreadLocal
站出來了,它在創(chuàng)建子線程的過程中
拷貝了父親線程中的inheritableThreadLocals
數(shù)據(jù),在new Thread()
代碼中,有一段這樣的代碼:
Thread parent = currentThread(); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
但是在真實(shí)的項(xiàng)目當(dāng)中,異步操作幾乎都是用的線程池來處理,也就意味著線程是復(fù)用的,這就導(dǎo)致了不同任務(wù)的上下文使用的是同一個(gè)線程的上下文,這就會(huì)導(dǎo)致程序出現(xiàn)意料不到的BUG
。
針對(duì)這種情況,我們發(fā)現(xiàn)應(yīng)該把線程上下文轉(zhuǎn)變成任務(wù)上下文,這樣的話才能避免多個(gè)任務(wù)共用一個(gè)線程上下文,為此我們不得不封裝一下每一個(gè)傳入線程池的任務(wù):
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(); } } }
但是這樣做確實(shí)不是很優(yōu)雅,所以為何不用TransmittableThreadLocal
試試呢?
示例
我們來通過一個(gè)示例演示一下TransmittableThreadLocal
是否能夠在線程池中實(shí)現(xiàn)上下文的傳遞,并且滿足任務(wù)間上下文的隔離效果:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>(); // 使用只有一個(gè)線程的線程池,測試線程復(fù)用是否影響TransmittableThreadLocal的效果 private static final Executor EXECUTOR = Executors.newFixedThreadPool(1); public static void main(String[] args) throws InterruptedException { // 設(shè)置主線程的上下文為"china" CONTEXT.set("china"); // 創(chuàng)建第一個(gè)任務(wù),通過TtlRunnable.get()包裝; // 在第一個(gè)任務(wù)中查看上下文數(shù)據(jù),檢查是否拿到正確的上下文; // 另外再修改掉該上下文,主要測試是否會(huì)影響第二個(gè)任務(wù)的上下文; Runnable task1 = TtlRunnable.get(() -> { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "開始"); String countryCode = CONTEXT.get(); System.out.println("第一個(gè)任務(wù)執(zhí)行結(jié)果:" + countryCode); // 修改該線程中上下文值,檢查是否影響第二個(gè)任務(wù) CONTEXT.set("US"); System.out.println(thread.getName() + "結(jié)束"); }); // 第二個(gè)任務(wù)主要測試上下文是否受第一個(gè)任務(wù)的影響 Runnable task2 = TtlRunnable.get(() -> { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "開始"); String countryCode = CONTEXT.get(); System.out.println("第二個(gè)任務(wù)執(zhí)行結(jié)果:" + countryCode); System.out.println(thread.getName() + "結(jié)束"); }); // 按順序執(zhí)行兩個(gè)任務(wù),全部放到線程池中執(zhí)行 CompletableFuture.runAsync(task1, EXECUTOR1) .thenRunAsync(task2, EXECUTOR1); // 檢查主線程上下文是否受影響; String countryCode = CONTEXT.get(); System.out.println("主線程執(zhí)行結(jié)果:" + countryCode); Thread.sleep(10000); }
1.我們準(zhǔn)備了只有一個(gè)線程的線程池,主要測試線程復(fù)用的情況;
2.準(zhǔn)備了兩個(gè)任務(wù),第一個(gè)任務(wù)檢查是否能夠拿到正確的上下文數(shù)據(jù);第二個(gè)任務(wù)測試是否因?yàn)榈谝粋€(gè)任務(wù)修改上下文受到影響;
執(zhí)行結(jié)果如下:
pool-1-thread-1開始
第一個(gè)任務(wù)執(zhí)行結(jié)果:china
pool-1-thread-1結(jié)束
pool-1-thread-1開始
第二個(gè)任務(wù)執(zhí)行結(jié)果:china
pool-1-thread-1結(jié)束
主線程執(zhí)行結(jié)果:china
通過上述示例,我們可以得出以下結(jié)論:
1.TransmittableThreadLocal
可以讓線程池中的上下文保持和父線程一致;
2.TransmittableThreadLocal
解決了線程復(fù)用導(dǎo)致多任務(wù)共享同一個(gè)線程上下文的問題;
使用方式
包裝任務(wù)
- 通過上述示例,我們學(xué)到了最基本的一種使用方式:
TtlRunnable.get()
,它可以用來包裝Runnable
接口的所有實(shí)例; - 同樣的,針對(duì)
Callable
下的實(shí)例,我們可以使用TtlCallable.get()
來包裝
包裝線程池
為了我們?cè)谑褂镁€程池時(shí),不用每次都使用TtlRunnable
或TtlCallable
來包裝所有任務(wù),TransmittableThreadLocal
還提供了包裝線程池的方法:
TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
通過包裝好的線程池,我們可以修改一下上面的示例代碼:
private static TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<>(); // 使用只有一個(gè)線程的線程池,測試線程復(fù)用是否影響TransmittableThreadLocal的效果 private static final Executor EXECUTOR = TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1)); public static void main(String[] args) throws InterruptedException { // 設(shè)置主線程的上下文為"china" CONTEXT.set("china"); // 創(chuàng)建第一個(gè)任務(wù),通過TtlRunnable.get()包裝; // 在第一個(gè)任務(wù)中查看上下文數(shù)據(jù),檢查是否拿到正確的上下文; // 另外再修改掉該上下文,主要測試是否會(huì)影響第二個(gè)任務(wù)的上下文; Runnable task1 = () -> { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "開始"); String countryCode = CONTEXT.get(); System.out.println("第一個(gè)任務(wù)執(zhí)行結(jié)果:" + countryCode); // 修改該線程中上下文值,檢查是否影響第二個(gè)任務(wù) CONTEXT.set("US"); System.out.println(thread.getName() + "結(jié)束"); }; // 第二個(gè)任務(wù)主要測試上下文是否受第一個(gè)任務(wù)的影響 Runnable task2 = () -> { Thread thread = Thread.currentThread(); System.out.println(thread.getName() + "開始"); String countryCode = CONTEXT.get(); System.out.println("第二個(gè)任務(wù)執(zhí)行結(jié)果:" + countryCode); System.out.println(thread.getName() + "結(jié)束"); }; // 按順序執(zhí)行兩個(gè)任務(wù),全部放到線程池中執(zhí)行 CompletableFuture.runAsync(task1, EXECUTOR1) .thenRunAsync(task2, EXECUTOR1); // 檢查主線程上下文是否受影響; String countryCode = CONTEXT.get(); System.out.println("主線程執(zhí)行結(jié)果:" + countryCode); Thread.sleep(10000); }
1.可以看出,我們包裝好線程池后,就不再需要包裝任務(wù)了,所有的任務(wù)都不需要TtlRunnable.get()
;
2.從包裝好的線程池中我們可以發(fā)現(xiàn),返回的實(shí)例其實(shí)是ExecutorTtlWrapper
對(duì)象,里面的submit
方法、execute()
方法上把傳進(jìn)去Runnable
參數(shù)使用TtlRunnable.get()
做了一層包裝;
小結(jié)
本文從業(yè)務(wù)角度切入,通過層層遞進(jìn)的方式從ThreadLocal
、InheritableThreadLocal
在業(yè)務(wù)上的應(yīng)用及產(chǎn)生的相關(guān)問題點(diǎn),逐步引出TransmittableThreadLocal
,通過示例的方式驗(yàn)證TransmittableThreadLocal
符合我們的需求,并且了解了TransmittableThreadLocal
針對(duì)任務(wù)及線程池的使用方式:
1.針對(duì)任務(wù)Runnable
、Callable
實(shí)例,使用TtlRunnable.get()
、TtlCallable.get()
包裝;
2.針對(duì)線程池,使用TtlExecutors.getTtlExecutor()
包裝;
以上就是TransmittableThreadLocal解決線程間上下文傳遞煩惱的詳細(xì)內(nèi)容,更多關(guān)于TransmittableThreadLocal線程傳遞的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java 基礎(chǔ)全面講解StringBuffer類的使用
當(dāng)對(duì)字符串進(jìn)行修改的時(shí)候,需要使用 StringBuffer 和 StringBuilder類,和String類不同的是,StringBuffer和 StringBuilder類的對(duì)象能夠被多次的修改,并且不產(chǎn)生新的未使用對(duì)象2022-01-01SpringBoot下載Excel文件時(shí),報(bào)錯(cuò)文件損壞的解決方案
這篇文章主要介紹了SpringBoot下載Excel文件時(shí),報(bào)錯(cuò)文件損壞的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06一次mybatis連接查詢遇到的坑實(shí)戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于一次mybatis連接查詢遇到的坑的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12關(guān)于SpringMVC對(duì)Restful風(fēng)格的支持詳解
Restful就是一個(gè)資源定位及資源操作的風(fēng)格,不是標(biāo)準(zhǔn)也不是協(xié)議,只是一種風(fēng)格,是對(duì)http協(xié)議的詮釋,下面這篇文章主要給大家介紹了關(guān)于SpringMVC對(duì)Restful風(fēng)格支持的相關(guān)資料,需要的朋友可以參考下2022-01-01maven引入mysql-connector-java包失敗的解決方案
這篇文章主要介紹了maven引入mysql-connector-java包失敗的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02基于java springboot + mybatis實(shí)現(xiàn)電影售票管理系統(tǒng)
這篇文章主要介紹了基于java springboot + mybatis實(shí)現(xiàn)的完整電影售票管理系統(tǒng)基于java springboot + mybatis,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08