Transmittable ThreadLocal(TTL)支持緩存線程池的 ThreadLocal
在使用線程池等會池化復(fù)用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能,解決異步執(zhí)行時上下文傳遞的問題。 一個Java標(biāo)準(zhǔn)庫本應(yīng)為框架/中間件設(shè)施開發(fā)提供的標(biāo)配能力,本庫功能聚焦 & 0依賴,支持Java 13/12/11/10/9/8/7/6。
JDK的InheritableThreadLocal類可以完成父線程到子線程的值傳遞。但對于使用線程池等會池化復(fù)用線程的執(zhí)行組件的情況,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的;這時父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒有意義,應(yīng)用需要的實際上是把 任務(wù)提交給線程池時的ThreadLocal值傳遞到 任務(wù)執(zhí)行時。
本庫提供的TransmittableThreadLocal類繼承并加強InheritableThreadLocal類,解決上述的問題,使用詳見User Guide。
整個TTL庫的核心功能(用戶API與框架/中間件的集成API、線程池ExecutorService/ForkJoinPool/TimerTask及其線程工廠的Wrapper),只有不到 1000 SLOC代碼行,非常精小。
歡迎 ??
- 建議和提問,提交
Issue - 貢獻(xiàn)和改進(jìn),
Fork后提通過Pull Request貢獻(xiàn)代碼
?? 需求場景
在ThreadLocal的需求場景即是TTL的潛在需求場景,如果你的業(yè)務(wù)需要『在使用線程池等會池化復(fù)用線程的執(zhí)行組件情況下傳遞ThreadLocal』則是TTL目標(biāo)場景。
下面是幾個典型場景例子。
- 分布式跟蹤系統(tǒng)
- 日志收集記錄系統(tǒng)上下文
-
Session級Cache - 應(yīng)用容器或上層框架跨應(yīng)用代碼給下層
SDK傳遞信息
各個場景的展開說明參見子文檔 需求場景。
?? User Guide
使用類TransmittableThreadLocal來保存值,并跨線程池傳遞。
TransmittableThreadLocal繼承InheritableThreadLocal,使用方式也類似。
相比InheritableThreadLocal,添加了
-
protected方法copy
用于定制 任務(wù)提交給線程池時 的ThreadLocal值傳遞到 任務(wù)執(zhí)行時 的拷貝行為,缺省傳遞的是引用。 -
protected方法beforeExecute/afterExecute
執(zhí)行任務(wù)(Runnable/Callable)的前/后的生命周期回調(diào),缺省是空操作。
具體使用方式見下面的說明。
1. 簡單使用
父線程給子線程傳遞值。
示例代碼:
// 在父線程中設(shè)置 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>(); context.set("value-set-in-parent"); // ===================================================== // 在子線程中可以讀取,值是"value-set-in-parent" String value = context.get();
# 完整可運行的Demo代碼參見SimpleDemo.kt。
這是其實是InheritableThreadLocal的功能,應(yīng)該使用InheritableThreadLocal來完成。
但對于使用線程池等會池化復(fù)用線程的執(zhí)行組件的情況,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的;這時父子線程關(guān)系的ThreadLocal值傳遞已經(jīng)沒有意義,應(yīng)用需要的實際上是把 任務(wù)提交給線程池時的ThreadLocal值傳遞到 任務(wù)執(zhí)行時。
解決方法參見下面的這幾種用法。
2. 保證線程池中傳遞值
2.1 修飾Runnable和Callable
使用TtlRunnable和TtlCallable來修飾傳入線程池的Runnable和Callable。
示例代碼:
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>(); context.set("value-set-in-parent"); Runnable task = new RunnableTask(); // 額外的處理,生成修飾了的對象ttlRunnable Runnable ttlRunnable = TtlRunnable.get(task); executorService.submit(ttlRunnable); // ===================================================== // Task中可以讀取,值是"value-set-in-parent" String value = context.get();
上面演示了Runnable,Callable的處理類似
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>(); context.set("value-set-in-parent"); Callable call = new CallableTask(); // 額外的處理,生成修飾了的對象ttlCallable Callable ttlCallable = TtlCallable.get(call); executorService.submit(ttlCallable); // ===================================================== // Call中可以讀取,值是"value-set-in-parent" String value = context.get();
# 完整可運行的Demo代碼參見TtlWrapperDemo.kt。
整個過程的完整時序圖
2.2 修飾線程池
省去每次Runnable和Callable傳入線程池時的修飾,這個邏輯可以在線程池中完成。
通過工具類com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:
-
getTtlExecutor:修飾接口Executor -
getTtlExecutorService:修飾接口ExecutorService -
getTtlScheduledExecutorService:修飾接口ScheduledExecutorService
示例代碼:
ExecutorService executorService = ... // 額外的處理,生成修飾了的對象executorService executorService = TtlExecutors.getTtlExecutorService(executorService); TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>(); 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();
# 完整可運行的Demo代碼參見TtlExecutorWrapperDemo.kt。
2.3 使用Java Agent來修飾JDK線程池實現(xiàn)類
這種方式,實現(xiàn)線程池的傳遞是透明的,業(yè)務(wù)代碼中沒有修飾Runnable或是線程池的代碼。即可以做到應(yīng)用代碼 無侵入。
# 關(guān)于 無侵入 的更多說明參見文檔Java Agent方式對應(yīng)用代碼無侵入。
示例代碼:
// ## 1. 框架上層邏輯,后續(xù)流程框架調(diào)用業(yè)務(wù) ## TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>(); context.set("value-set-in-parent"); // ## 2. 應(yīng)用邏輯,后續(xù)流程業(yè)務(wù)調(diào)用框架下層邏輯 ## 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();
Demo參見AgentDemo.kt。執(zhí)行工程下的腳本scripts/run-agent-demo.sh即可運行Demo。
目前TTL Agent中,修飾了的JDK執(zhí)行器組件(即如線程池)如下:
-
java.util.concurrent.ThreadPoolExecutor和java.util.concurrent.ScheduledThreadPoolExecutor- 修飾實現(xiàn)代碼在
TtlExecutorTransformlet.java。
- 修飾實現(xiàn)代碼在
-
java.util.concurrent.ForkJoinTask(對應(yīng)的執(zhí)行器組件是java.util.concurrent.ForkJoinPool)- 修飾實現(xiàn)代碼在
TtlForkJoinTransformlet.java。從版本2.5.1開始支持。 - 注意:
Java 8引入的CompletableFuture與(并行執(zhí)行的)Stream底層是通過ForkJoinPool來執(zhí)行,所以支持ForkJoinPool后,TTL也就透明支持了CompletableFuture與Stream。??
- 修飾實現(xiàn)代碼在
-
java.util.TimerTask的子類(對應(yīng)的執(zhí)行器組件是java.util.Timer)- 修飾實現(xiàn)代碼在
TtlTimerTaskTransformlet.java。從版本2.7.0開始支持。 - 注意:從
2.11.2版本開始缺省開啟TimerTask的修飾(因為保證正確性是第一位,而不是最佳實踐『不推薦使用TimerTask』:);2.11.1版本及其之前的版本沒有缺省開啟TimerTask的修飾。 - 使用
Agent參數(shù)ttl.agent.enable.timer.task開啟/關(guān)閉TimerTask的修飾:-javaagent:path/to/transmittable-thread-local-2.x.x.jar=ttl.agent.enable.timer.task:true-javaagent:path/to/transmittable-thread-local-2.x.x.jar=ttl.agent.enable.timer.task:false
- 更多關(guān)于
TTL Agent參數(shù)的配置說明詳見TtlAgent.java的JavaDoc。
- 修飾實現(xiàn)代碼在
關(guān)于
java.util.TimerTask/java.util.Timer
Timer是JDK 1.3的老類,不推薦使用Timer類。推薦用
ScheduledExecutorService。
ScheduledThreadPoolExecutor實現(xiàn)更強壯,并且功能更豐富。 如支持配置線程池的大?。?code>Timer只有一個線程);Timer在Runnable中拋出異常會中止定時執(zhí)行。更多說明參見10. Mandatory Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions. - Alibaba Java Coding Guidelines。
關(guān)于boot class path設(shè)置
因為修飾了JDK標(biāo)準(zhǔn)庫的類,標(biāo)準(zhǔn)庫由bootstrap class loader加載;修飾后的JDK類引用了TTL的代碼,所以Java Agent使用方式下TTL Jar文件需要配置到boot class path上。
TTL從v2.6.0開始,加載TTL Agent時會自動設(shè)置TTL Jar到boot class path上。
注意:不能修改從Maven庫下載的TTL Jar文件名(形如transmittable-thread-local-2.x.x.jar)。 如果修改了,則需要自己手動通過-Xbootclasspath JVM參數(shù)來顯式配置(就像TTL之前的版本的做法一樣)。
自動設(shè)置TTL Jar到boot class path的實現(xiàn)是通過指定TTL Java Agent Jar文件里manifest文件(META-INF/MANIFEST.MF)的Boot-Class-Path屬性:
Boot-Class-PathA list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed.
更多詳見
Java Agent規(guī)范 -JavaDoc- JAR File Specification - JAR Manifest
- Working with Manifest Files - The Java? TutorialsHide
Java的啟動參數(shù)配置
在Java的啟動參數(shù)加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar。
如果修改了下載的TTL的Jar的文件名(transmittable-thread-local-2.x.x.jar),則需要自己手動通過-Xbootclasspath JVM參數(shù)來顯式配置:
比如修改文件名成ttl-foo-name-changed.jar,則還加上Java的啟動參數(shù):-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar
Java命令行示例如下:
java -javaagent:path/to/transmittable-thread-local-2.x.x.jar \
-cp classes \
com.alibaba.demo.ttl.agent.AgentDemo
或是
# 如果修改了TTL jar文件名 或 TTL版本是 2.6.0 之前, # 則還需要顯式設(shè)置 -Xbootclasspath 參數(shù) java -javaagent:path/to/ttl-foo-name-changed.jar \ -Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \ -cp classes \ com.alibaba.demo.ttl.agent.AgentDemo
?? Java API Docs
當(dāng)前版本的Java API文檔地址: https://alibaba.github.io/transmittable-thread-local/apidocs/
?? Maven依賴
示例:
<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.11.4</version> </dependency>
可以在 search.maven.org 查看可用的版本。
? FAQ
- Mac OS X下,使用javaagent,可能會報
JavaLaunchHelper的出錯信息。
JDK Bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=8021205
可以換一個版本的JDK。我的開發(fā)機上1.7.0_40有這個問題,1.6.0_51、1.7.0_45可以運行。
#1.7.0_45還是有JavaLaunchHelper的出錯信息,但不影響運行。
