跳到主要內容

搜尋酷無極

选择搜索范围,输入关键词后按回车。

語言 / Language

Java執行緒上下文

ThreadLocal是JDK java.lang套件下較常用的工具類別,實現了執行緒上下文的功能,最常見的應用應該就是用ThreadLocal來存放登入後的使用者上下文等關鍵資訊。

執行緒上下文還可以優雅地解決很多看起來比較複雜的問題,比如:spring-mybatis框架的整合問題:
在基於spring生態開發的時候,我們一般都是將交易委託給spring管理,透過transactional註解去設定開啟交易,以及交易的隔離層級等資訊。我們操作資料庫也不是直接透過jdbc,而是透過mybatis框架來存取資料庫。spring和mybatis是兩個完全獨立的框架,但是我們在使用的時候,似乎由spring開啟的資料庫connection直接就可以被mybatis取得使用,我們基本什麼也不用做,引入一個mybatis-spring-boot-stater的jar套件就可以了。
這個優雅的設計實現的關鍵點就是ThreadLocal,大體原理是stater jar套件最終會幫我們引入一個MybatisAutoConfiguration並透過SPI注入到我們服務的bean容器裡,在MybatisAutoConfiguration中設定好了mybatis需要的SqlSessionFactory,並注入了spring自己的SpringManagedTransactionFactory(實作了TransactionFactory,交易工廠是mybatis提供的擴充機制,可以把交易管理委派出去)。
整體的執行流程就是 transactional 註解的方法最終會被 AOP 代理,代理類別會在前置的通知中從 dataSource 取得一個 connection,並最終放到一個 ThreadLocal 中(其實 ThreadLocal 裡放的是一個 map,key 是 dataSource,value 是 connectionHolder 等同於 connection),後續 mybatis 執行 sql 時會先透過TransactionFactory取得connection,因為此時注入的TransactionFactory是SpringManagedTransactionFactory,所以會從 ThreadLocal 取出對應 dataSource 的 connection。

ThreadLocal簡單原理介紹:

tapd_50394669_1660562598_27.png

ThreadLocalMap就是一張採用線性探測法解決衝突的雜湊表,每個執行緒都會產生一份,每個 ThreadLocal 物件裡都有一個threadLocalHashCode用來計算雜湊鍵。
對比一下 HashMap,ThreadLocalMap選擇的衝突處理演算法是線性探測而不是鏈結法,可能基於以下考量:
1、記憶體考量,因為使用情境不同,ThreadLocalMap的資料量一般會遠小於HashMap,在小資料量的情況下,鏈結法會多出很多指標的空間消耗
2、效率,在1的基礎上,數量少的情況下省下來的空間可以用來產生一個填載因子較小的雜湊表,減少衝突,提高平均查找速度

ThreadLocal、執行緒和 ThreadLocalMap 關係圖

問題
A 在基於執行緒池的任務情境中,存在上下文傳遞問題,尤其是在基於Tomcat的web程式設計模型中,由於Tomcat本身是使用執行緒池來處理伺服器與用戶端間的請求與回應互動,每個web應用程式都不得不手動處理執行緒上下文正確傳遞問題。
B 效率問題,如果應用程式中的ThreadLocal過多,導致雜湊表膨脹以後,查找速度會下降

針對問題A,常見的解決方式比較統一,直接貼一下我們系統裡的程式碼
2.png
這種方案存在兩個問題:
1、如果後續有新增的ThreadLocal需要正確傳遞,則需要修改程式碼,在wrap邏輯中加上新的ThreadLocal,一旦遺漏就會出bug
2、在一些情境中,任務可能會由提交執行緒自行執行,比如:

  • 當執行緒池滿了且執行緒池的 RejectedExecutionHandler 使用的是 CallerRunsPolicy 時,提交到執行緒集區的任務會在提交執行緒中直接執行,此時最後的清除邏輯會導致提交執行緒的上下文遺失

  • 類似地,使用 ForkJoinPool (包含平行執行 Stream 與 CompletableFuture ,底層使用 ForkJoinPool )的情境,展開的 ForkJoinTask 會在任務提交執行緒中直接執行。最後的清除同樣會導致提交執行緒的上下文遺失

解決方案:
問題1:如果ThreadLocal類別支援一個API,可以取得應用程式裡現在一共有多少ThreadLocal物件,那麼問題就迎刃而解了,可惜不支援。硬要做的話,可以透過反射取得thread物件裡的inheritableThreadLocals(就是一個ThreadLocalMap)屬性,然後遍歷雜湊表取出所有的entry就可以得到所有threadlocal鍵值對。另一種思路就是對jdk的ThreadLocal再做一層封裝,增補上述的功能。
問題2:可以透過完善wrap的邏輯解決:在執行緒初始執行時先取得目前執行緒的上下文進行暫存,然後再用提交執行緒的上下文替換本執行緒的上下文,執行緒執行結束前不進行清除,而是改為使用暫存資料還原本執行緒的上下文。

transmittable-thread-local
已經有現成的工具了,實作原理就是從上面兩個方向著手的,並且給這個流程起了個名字 CRR(capture/replay/restore) 模式。
使用方式有三種:
1 ### 修飾 Runnable 和 Callable

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent"); 
Runnable task = new RunnableTask(); 
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);  
// =====================================================  
// Task中可以读取,值是"value-set-in-parent" 
String value = context.get();

2 修飾執行緒池

ExecutorService executorService = ... 
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);  
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent");  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// =====================================================  
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

3 ### 使用 Java Agent 來修飾 JDK 執行緒池實作類別

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## 
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); 
context.set("value-set-in-parent");  
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## 
ExecutorService executorService = Executors.newFixedThreadPool(3);  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// ## 3. 框架下层逻辑 ## 
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

使用java agent方式的好處是對應用程式碼無侵入,壞處的話我覺得最大的問題是如果啟動jvm忘了加參數,那可能就會出事故。

簡單看一下原始碼邏輯
3.png

4.png 

針對問題B,其實在應用程式開發中一般不會考慮這個問題,因為對時間效率的敏感度沒有那麼高。但是在一些底層的中介軟體開發中,對於效能最佳化可能就比較重視了。比如netty是選擇自行實作了FastThreadLocal代替jdk的ThreadLocal,同時提供了配套的FastThreadLocalThread,在FastThreadLocalThread和FastThreadLocal搭配使用的情況下,可以始終保證O(1)查找時間複雜度,它的解決方案原理就是以空間換時間,為了避免雜湊表出現雜湊衝突後降低查詢效率,會產生一個足夠長的陣列,每個FastThreadLocal建構時都會分配一個唯一的陣列索引,相當於一人一個位置,查詢時拿著自己的index直接從陣列中取就可以了。
簡單貼一下原始碼
5.png


評論區

傳送中

選擇喜歡的樣式

所選樣式會套用至全站。