前言
在做分布式鏈路追蹤系統(tǒng)的時候要糊,需要解決異步調(diào)用透傳上下文的需求,特別是傳遞traceId,本文就線程池透傳幾種方式進(jìn)行分析货岭。
其他典型場景例子:
分布式跟蹤系統(tǒng) 或 全鏈路壓測(即鏈路打標(biāo))
日志收集記錄系統(tǒng)上下文
Session
級Cache
應(yīng)用容器或上層框架跨應(yīng)用代碼給下層
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考慮了這種場景桩撮,實現(xiàn)了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);//等待子線程執(zhí)行完
System.out.println("thread:" + itl.get());
上面的輸出是:
subThread:father
//子線程可以拿到父線程的變量
son
thread:father
//子線程修改不影響父線程的變量
1.2 InheritableThreadLocal的實現(xiàn)原理
有同學(xué)可能想知道InheritableThreadLocal的實現(xiàn)原理,其實特別簡單芜果。就是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
類可以完成父線程到子線程的值傳遞霹粥。但對于使用線程池等會池化復(fù)用線程的執(zhí)行組件的情況灭将,線程由線程池創(chuàng)建好,并且線程是池化起來反復(fù)使用的后控;這時父子線程關(guān)系的ThreadLocal
值傳遞已經(jīng)沒有意義庙曙,應(yīng)用需要的實際上是把 任務(wù)提交給線程池時的ThreadLocal
值傳遞到 任務(wù)執(zhí)行時。
2浩淘、日志MDC/Opentracing的實現(xiàn)
如果你的應(yīng)用實現(xiàn)了Opentracing的規(guī)范捌朴,比如通過skywalking
的agent對線程池做了攔截,那么自定義Scope實現(xiàn)類张抄,可以跨線程傳遞MDC砂蔽,然后你的義務(wù)可以通過設(shè)置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依賴),提供了增強(qiáng)的InheritableThreadLocal极谊,即使使用線程池組件也可以在線程之間傳輸值诡右。
3.1 transmittable-thread-local 官方readme參考:
使用類TransmittableThreadLocal
來保存值,并跨線程池傳遞轻猖。
TransmittableThreadLocal
繼承InheritableThreadLocal
帆吻,使用方式也類似。相比InheritableThreadLocal
咙边,添加了
-
copy
方法
用于定制 任務(wù)提交給線程池時 的ThreadLocal
值傳遞到 任務(wù)執(zhí)行時 的拷貝行為猜煮,缺省傳遞的是引用。
注意:如果跨線程傳遞了對象引用因為不再有線程封閉败许,與InheritableThreadLocal.childValue
一樣友瘤,使用者/業(yè)務(wù)邏輯要注意傳遞對象的線程
-
protected
的beforeExecute
/afterExecute
方法
執(zhí)行任務(wù)(Runnable
/Callable
)的前/后的生命周期回調(diào),缺省是空操作檐束。
3.2 transmittable-thread-local 代碼例子
方式一:TtlRunnable封裝:
ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父線程中設(shè)置
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被丧,無代碼入侵
這種方式盟戏,實現(xiàn)線程池的傳遞是透明的绪妹,業(yè)務(wù)代碼中沒有修飾Runnable
或是線程池的代碼。即可以做到應(yīng)用代碼 無侵入柿究。
ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父線程中設(shè)置
context.set("value-set-in-parent");
executorService.submit(() -> {
System.out.println(context.get());
});
4邮旷、grpc的實現(xiàn)
grpc是一種分布式調(diào)用協(xié)議和實現(xiàn),也封裝了一套跨線程傳遞上下文的實現(xiàn)蝇摸。
io.grpc.Context
表示上下文婶肩,用來在一次grpc請求鏈路中傳遞用戶登錄信息、tracing信息等貌夕。
Context常用用法如下律歼。首先獲取當(dāng)前context,這個一般是作為參數(shù)傳過來的,或通過current()獲取當(dāng)前的已有context啡专。
然后通過attach方法险毁,綁定到當(dāng)前線程上,并且返回當(dāng)前線程
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自己们童,從而進(jìn)入到一個新的scope中畔况,新的scope以此Context實例作為current,并且返回之前的current context
- detach(Context toDetach) attach()方法的反向方法慧库,退出當(dāng)前Context并且detach到toDetachContext跷跪,每個attach方法要對應(yīng)一個detach,所以一般通過try finally代碼塊或wrap模板方法來使用齐板。
- static storage() 獲取storage吵瞻,Storage是用來attach和detach當(dāng)前context用的。
線程池傳遞實現(xiàn):
ExecutorService executorService = Executors.newCachedThreadPool();
Context.withValue("key","value");
execute(Context.current().wrap(() -> {
System.out.println(Context.current().getValue("key"));
}));
5覆积、總結(jié)
以上總結(jié)的四種實現(xiàn)跨線程傳遞的方法听皿,最簡單的就是自己定義一個Runnable熟呛,添加屬性傳遞即可宽档。如果考慮通用型,需要中間件封裝一個Executor對象庵朝,類似transmittable-thread-local的實現(xiàn)吗冤,或者直接使用transmittable-thread-local。
實踐的項目中九府,考慮周全椎瘟,要支持span、MDC侄旬、rpc上下文肺蔚、業(yè)務(wù)自定義上下文,可以參考以上方法封裝儡羔。
參考資料
[grpc源碼分析1-context] https://www.codercto.com/a/66559.html
[threadlocal變量透傳宣羊,這些問題你都遇到過嗎璧诵?]https://cloud.tencent.com/developer/article/1492379
本文由猿必過 YBG 發(fā)布
禁止未經(jīng)授權(quán)轉(zhuǎn)載,違者依法追究相關(guān)法律責(zé)任
如需授權(quán)可聯(lián)系:zhuyunhui@yuanbiguo.com