背景
新的工作新的開始,先描述下問題的背景,項目中為了解決多數據源聚合快速響應問題,啟用線程池并發調用多數據源服務獲取數據,做聚合接口對外輸出,同時也帶來了問題,日志跟蹤需要跟蹤線程池服務調用及數據處理,為了不影響原有的方法參數列表,采用ThreadLocal進行了日志鏈路追蹤,有時候會產生根據ThreadLocal設置的traceId線程池執行后無法快速定位單個服務調用所產生的日志,只能通過上下文去人肉排查,對于程序員是件很苦逼的事情。感謝兒子今天借我用一下他的電腦...
調研步驟:
1.ThreadLocal 為什么沒有能傳遞traceId?
2.jdk本身是否有適應這種場景的線程變量去處理父子線程的問題?
3.是否有開源框架已經解決了這樣的問題?
4.總結
1.TheadLocal為啥沒能傳遞traceId?
這個問題很好解釋,ThreadLocal本身是線程的內部變量,隸屬于線程本身,不能跨線程傳輸數據。
2.jdk本身是否有適應這種場景的線程變量去處理父子線程的問題?
Thread.class中提供了另外一個變量InheritableThreadLocal,通過分析Thread源碼可看到如下代碼片段:
Thread類中聲明了一個ThreadLocalMap 變量
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在子線程創建時,會將父線程的inheritableThreadLocals賦值到子線程中。
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
.........
/**
* 此處為父子線程在初始化線程時賦值的過程
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
........
/* Set thread ID */
tid = nextThreadID();
}
代碼塊第二個地方Thread#init方法,說明只能在父線程創建子線程時,能夠實現父子線程之間通過threadLocal傳值。如果像線程池這種有可能復用線程的情形,則會出現無法傳遞的問題。到此發現問題可能沒有想象的簡單。
3.是否有開源框架已經解決了這樣的問題?
既然問題這么明顯,是否有前輩已經解決了呢,如果有的話是否有熱心大神開源了,找了一下果然找到了大神的真跡。transmittable-thread-local 阿里開源
TransmittableThreadLocal是阿里開源的庫,繼承了InheritableThreadLocal,優化了在使用線程池等會池化復用線程的情況下傳遞ThreadLocal的使用。
簡單來說,有個專門的TtlRunnable和TtlCallable包裝類,用于讀取原Thread的ThreadLocal對象及值并存于Runnable/Callable中,在執行run或者call方法的時候再將存于Runnable/Callable中的ThreadLocal對象和值讀取出來,存入調用run或者call的線程中。
TransmittableThreadLocal 調用時序如下:
TransmittableThreadLocal的通過修飾線程池的使用方式
省去每次Runnable
和Callable
傳入線程池時的修飾,這個邏輯可以在線程池中完成。通過工具類com.alibaba.ttl.threadpool.TtlExecutors
完成,有下面的方法:
-
getTtlExecutor
:修飾接口Executor
-
getTtlExecutorService
:修飾接口ExecutorService
-
getTtlScheduledExecutorService
:修飾接口ScheduledExecutorService
TtlExecutors使用demo.png
使用Java Agent植入修飾代碼
Java Agent(Instrumentation)是JDK1.5引入的技術,基于JVM TI機制,使得開發者可以構建一個獨立于應用程序的代理(Agent),用來監測和協助運行在 JVM 上的程序,以及替換和修改某些類的定義。開發者可以在一個普通 Java 程序運行時,通過 – javaagent 參數指定一個特定的 jar 文件(包含 Instrumentation 代理)來啟動相應的代理程序,植入自己擴展的修飾代碼以實現功能。
// ## 1. 框架上層邏輯,后續流程框架調用業務 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<String>();
context.set("value-set-in-parent");
// ## 2. 應用邏輯,后續流程業務調用框架下層邏輯 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = new Task("1");
Callable call = new Call("2");
executorService.submit(task);
executorService.submit(call);
// ## 3. 框架下層邏輯 ##
// Task或是Call中可以讀取,值是"value-set-in-parent"
String value = context.get();
Maven依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.10.2</version>
</dependency>
Java的啟動參數配置
在Java的啟動參數加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar。
如果修改了下載的TTL的Jar的文件名(transmittable-thread-local-2.x.x.jar),則需要自己手動通過-Xbootclasspath JVM參數來顯式配置:
比如修改文件名成ttl-foo-name-changed.jar,則還加上Java的啟動參數:
-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.ttl.threadpool.agent.demo.AgentDemo
或是
java -javaagent:path/to/ttl-foo-name-changed.jar \
-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \
-cp classes \
com.alibaba.ttl.threadpool.agent.demo.AgentDemo
將封裝好的TransmittableThreadLocal Jar包放在類目錄下的某個文件夾下,例如agent,那么只需在啟動參數加入:-javaagent:agent/transmittable-thread-local-xxx.jar即可完成修飾代碼的植入。
4.總結
1、引入TransmittableThreadLocalj.jar ,通過TtlExecutors包裝現有線程池,使用TransmittableThreadLocal代替InheritableThreadLocal傳值,解決線程池復用導致的threadLocal值丟失問題,有一定的工作量。
2、通過java Agent 無侵入解決此問題,工作量小,效率高,需要運維的支持,而且對探針技術未實際使用過,存在一定風險。
ps:對于java Agent 還沒研究過,后續研究透徹補充進去