
前言
在做分布式鏈路追蹤系統的時候,需要解決異步呼叫透傳背景關系的需求,特別是傳遞traceId,本文就執行緒池透傳幾種方式進行分析,
其他典型場景例子:
-
分布式跟蹤系統 或 全鏈路壓測(即鏈路打標)
-
日志收集記錄系統背景關系
-
Session級Cache -
應用容器或上層框架跨應用代碼給下層
SDK傳遞資訊
1、JDK對跨執行緒傳遞ThreadLocal的支持
首先看一個最簡單場景,也是一個錯誤的例子,
void testThreadLocal(){
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set("not ok");
new Thread(()->{
System.out.println(threadLocal.get());
}).start();
}
java中的threadlocal,是系結在執行緒上的,你在一個執行緒中set的值,在另外一個執行緒是拿不到的,
上面的輸出是:
null
1.1 InheritableThreadLocal 例子
JDK考慮了這種場景,實作了InheritableThreadLocal ,不要高興太早,這個只是支持父子執行緒,執行緒池會有問題,
我們看下InheritableThreadLocal的例子:
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("father");
new Thread(()->{
System.out.println("subThread:" + itl.get());
itl.set("son");
System.out.println(itl.get());
}).start();
Thread.sleep(500);//等待子執行緒執行完
System.out.println("thread:" + itl.get());
上面的輸出是:
subThread:father //子執行緒可以拿到父執行緒的變數
son
thread:father //子執行緒修改不影響父執行緒的變數
1.2 InheritableThreadLocal的實作原理
有同學可能想知道InheritableThreadLocal的實作原理,其實特別簡單,就是Thread類里面分開記錄了ThreadLocal、InheritableThreadLocal的ThreadLocalMap,初始化的時候,會拿到parent.InheritableThreadLocal,直接上代碼可以看的很清楚,
class Thread {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
JDK的InheritableThreadLocal類可以完成父執行緒到子執行緒的值傳遞,但對于使用執行緒池等會池化復用執行緒的執行組件的情況,執行緒由執行緒池創建好,并且執行緒是池化起來反復使用的;這時父子執行緒關系的ThreadLocal值傳遞已經沒有意義,應用需要的實際上是把 任務提交給執行緒池時的ThreadLocal值傳遞到 任務執行時,
2、日志MDC/Opentracing的實作
如果你的應用實作了Opentracing的規范,比如通過skywalking的agent對執行緒池做了攔截,那么自定義Scope實作類,可以跨執行緒傳遞MDC,然后你的義務可以通過設定MDC的值,傳遞給子執行緒,
代碼如下:
this.scopeManager = scopeManager;
this.wrapped = wrapped;
this.finishOnClose = finishOnClose;
this.toRestore = (OwlThreadLocalScope)scopeManager.tlsScope.get();
scopeManager.tlsScope.set(this);
if (wrapped instanceof JaegerSpan) {
this.insertMDC(((JaegerSpan)wrapped).context());
} else if (wrapped instanceof JaegerSpanWrapper) {
this.insertMDC(((JaegerSpanWrapper)wrapped).getDelegated().context());
}
3、阿里transmittable-thread-local
github地址:https://github.com/alibaba/transmittable-thread-local
TransmittableThreadLocal(TTL)是框架/中間件缺少的Java?std lib(簡單和0依賴),提供了增強的InheritableThreadLocal,即使使用執行緒池組件也可以在執行緒之間傳輸值,
3.1 transmittable-thread-local 官方readme參考:
使用類TransmittableThreadLocal來保存值,并跨執行緒池傳遞,
TransmittableThreadLocal繼承InheritableThreadLocal,使用方式也類似,相比InheritableThreadLocal,添加了
-
copy方法
用于定制 任務提交給執行緒池時 的ThreadLocal值傳遞到 任務執行時 的拷貝行為,預設傳遞的是參考,
注意:如果跨執行緒傳遞了物件參考因為不再有執行緒封閉,與InheritableThreadLocal.childValue一樣,使用者/業務邏輯要注意傳遞物件的執行緒 -
protected的beforeExecute/afterExecute方法
執行任務(Runnable/Callable)的前/后的生命周期回呼,預設是空操作,
3.2 transmittable-thread-local 代碼例子
方式一:TtlRunnable封裝:
ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父執行緒中設定
context.set("value-set-in-parent");
// 額外的處理,生成修飾了的物件ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(() -> {
System.out.println(context.get());
});
executorService.submit(ttlRunnable);
方式二:ExecutorService封裝:
ExecutorService executorService = ...
// 額外的處理,生成修飾了的物件executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);
方式三:使用java agent,無代碼入侵
這種方式,實作執行緒池的傳遞是透明的,業務代碼中沒有修飾Runnable或是執行緒池的代碼,即可以做到應用代碼 無侵入,
ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父執行緒中設定
context.set("value-set-in-parent");
executorService.submit(() -> {
System.out.println(context.get());
});
4、grpc的實作
grpc是一種分布式呼叫協議和實作,也封裝了一套跨執行緒傳遞背景關系的實作,
io.grpc.Context 表示背景關系,用來在一次grpc請求鏈路中傳遞用戶登錄資訊、tracing資訊等,
Context常用用法如下,首先獲取當前context,這個一般是作為引數傳過來的,或通過current()獲取當前的已有context,
然后通過attach方法,系結到當前執行緒上,并且回傳當前執行緒
public Runnable wrap(final Runnable r) {
return new Runnable() {
@Override
public void run() {
Context previous = attach();
try {
r.run();
} finally {
detach(previous);
}
}
};
}
Context的主要方法如下
- attach() attach Context自己,從而進入到一個新的scope中,新的scope以此Context實體作為current,并且回傳之前的current context
- detach(Context toDetach) attach()方法的反向方法,退出當前Context并且detach到toDetachContext,每個attach方法要對應一個detach,所以一般通過try finally代碼塊或wrap模板方法來使用,
- static storage() 獲取storage,Storage是用來attach和detach當前context用的,
執行緒池傳遞實作:
ExecutorService executorService = Executors.newCachedThreadPool();
Context.withValue("key","value");
execute(Context.current().wrap(() -> {
System.out.println(Context.current().getValue("key"));
}));
5、總結
以上總結的四種實作跨執行緒傳遞的方法,最簡單的就是自己定義一個Runnable,添加屬性傳遞即可,如果考慮通用型,需要中間件封裝一個Executor物件,類似transmittable-thread-local的實作,或者直接使用transmittable-thread-local,
實踐的專案中,考慮周全,要支持span、MDC、rpc背景關系、業務自定義背景關系,可以參考以上方法封裝,
參考資料
[grpc原始碼分析1-context] https://www.codercto.com/a/66559.html
[threadlocal變數透傳,這些問題你都遇到過嗎?]https://cloud.tencent.com/developer/article/1492379
掃描二維碼,關注公眾號“猿必過”

回復 “面試題” 自行領取吧,
微信群交流討論,請添加微信號:zyhui98,備注:面試題加群
本文由猿必過 YBG 發布
禁止未經授權轉載,違者依法追究相關法律責任
如需授權可聯系:[email protected]
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/266902.html
標籤:Java
下一篇:第一天
