在作分佈式鏈路追蹤系統的時候,須要解決異步調用透傳上下文的需求,特別是傳遞traceId,本文就線程池透傳幾種方式進行分析。html
其餘典型場景例子:java
分佈式跟蹤系統 或 全鏈路壓測(即鏈路打標)git
日誌收集記錄系統上下文github
Session
級Cache
web
應用容器或上層框架跨應用代碼給下層SDK
傳遞信息面試
首先看一個最簡單場景,也是一個錯誤的例子。api
void testThreadLocal(){ ThreadLocal<Object> threadLocal = new ThreadLocal<>(); threadLocal.set("not ok"); new Thread(()->{ System.out.println(threadLocal.get()); }).start(); }
java中的threadlocal,是綁定在線程上的。你在一個線程中set的值,在另一個線程是拿不到的。微信
上面的輸出是:oracle
null
app
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
//子線程修改不影響父線程的變量
有同窗可能想知道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
值傳遞到 任務執行時。
若是你的應用實現了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()); }
github地址:https://github.com/alibaba/transmittable-thread-local
TransmittableThreadLocal(TTL)是框架/中間件缺乏的Java™std lib(簡單和0依賴),提供了加強的InheritableThreadLocal,即便使用線程池組件也能夠在線程之間傳輸值。
使用類TransmittableThreadLocal
來保存值,並跨線程池傳遞。
TransmittableThreadLocal
繼承InheritableThreadLocal
,使用方式也相似。相比InheritableThreadLocal
,添加了
copy
方法
用於定製 任務提交給線程池時 的ThreadLocal
值傳遞到 任務執行時 的拷貝行爲,缺省傳遞的是引用。
注意:若是跨線程傳遞了對象引用由於再也不有線程封閉,與InheritableThreadLocal.childValue
同樣,使用者/業務邏輯要注意傳遞對象的線程
protected
的beforeExecute
/afterExecute
方法
執行任務(Runnable
/Callable
)的前/後的生命週期回調,缺省是空操做。
方式一: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()); });
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的主要方法以下
線程池傳遞實現:
ExecutorService executorService = Executors.newCachedThreadPool(); Context.withValue("key","value"); execute(Context.current().wrap(() -> { System.out.println(Context.current().getValue("key")); }));
以上總結的四種實現跨線程傳遞的方法,最簡單的就是本身定義一個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 發佈
禁止未經受權轉載,違者依法追究相關法律責任
如需受權可聯繫:zhuyunhui@yuanbiguo.com