InheritableThreadLocal NullPointException & 線程池環(huán)境下使用TTL進(jìn)行線程上下文傳遞
背景:因?yàn)闃I(yè)務(wù)需要,在某個(gè)接口處理超過3秒逛裤,就即時(shí)返回。因此我使用了Future的 超時(shí)特性猴抹。然后我又用線程池去處理Future任務(wù)带族。同時(shí)我之前又加了一個(gè)切面,那么切面有一個(gè)InheritableThreadLocal變量蟀给,用于存放請求上下文信息蝙砌。原來是ThreadLocal,但是子線程也需要用到跋理,所以切換到了InheritableThreadLocal择克。就是這個(gè)InheritableThreadLocal在線程池中導(dǎo)致了NPE問題
關(guān)鍵詞:InheritableThreadLocal 空指針異常 Future NPE
原因
InheritableThreadLocal 無法在線程池中獲取到父線程的信息。我這里前普,F(xiàn)uture 是在線程池中執(zhí)行的肚邢,所以導(dǎo)致了InheritableThreadLocal 拋出 NullPointException
解決方案
先說解決方案
不使用線程池,每次執(zhí)行都new 一個(gè)線程拭卿。
-
使用阿里的 transmittable-thread-local
-
引入maven
<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.12.4</version> </dependency>
-
修飾Runnable和Callable
Runnable
// 全局變量 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); // 使用TTL傳遞 /*------------------在父線程中設(shè)置---------------------*/ context.set("value-set-in-parent"); /*------------------在父線程中設(shè)置---------------------*/ /*------------------額外的處理---------------------*/ Runnable task = new RunnableTask(); // 額外的處理骡湖,生成修飾了的對象ttlRunnable Runnable ttlRunnable = TtlRunnable.get(task); executorService.submit(ttlRunnable); /*------------------額外的處理---------------------*/ /*------------------在子線程中使用---------------------*/ // Task中可以讀取,值是"value-set-in-parent" String value = context.get(); /*------------------在子線程中使用---------------------*/
Callable
// 全局變量 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); // 使用TTL傳遞 /*------------------在父線程中設(shè)置---------------------*/ context.set("value-set-in-parent"); /*------------------在父線程中設(shè)置---------------------*/ /*------------------額外的處理---------------------*/ Callable call = new CallableTask(); // 額外的處理峻厚,生成修飾了的對象ttlRunnable Callable ttlCallable = TtlCallable.get(call); executorService.submit(ttlCallable); /*------------------額外的處理---------------------*/ /*------------------在子線程中使用---------------------*/ // Task中可以讀取勺鸦,值是"value-set-in-parent" String value = context.get(); /*------------------在子線程中使用---------------------*/
-
分析
我喜歡刨根問底,沒有得到本質(zhì)目木,到時(shí)候還是會忘記的换途。這里只分析通信過程懊渡,忽略保證線程安全等細(xì)節(jié)東西。這樣不會影響主流程
線程之間如何通過InheritableThreadLocal & ThreadLocal 通信军拟?
看源碼可以看到剃执,ThreadLocal 本質(zhì)只是操作 Thread 類的 threadLocals 成員變量的一個(gè)工具而已。
Thread的對2個(gè)成員變量的說明:
/* 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;
而 InheritableThreadLocal 繼承了 ThreadLocal 懈息,和父類ThreadLocal 本質(zhì)沒有什么區(qū)別肾档,只是變成了操作 Thread類 的 inheritableThreadLocals 成員變量。
ThreadLocal對應(yīng)代碼:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 初始化map的方法 get 或者 set的時(shí)候如果補(bǔ)存在就會初始化
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
InheritableThreadLocal重寫了父類ThreadLocal的 createMap 和 getMap 方法辫继,從而在初始化和賦值的時(shí)候怒见,變成了操作 inheritableThreadLocals
InheritableThreadLocal 對應(yīng)的代碼:
// 僅僅是重寫了ThreadLocal的getMap和createMap方法
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
大致流程
因?yàn)闀r(shí)間有限,我只看了主流程姑宽,細(xì)枝末節(jié)還有細(xì)節(jié)得東西我都忽略了遣耍,并且是我個(gè)人得總結(jié),可能有不少紕漏炮车,僅供參考
我推測ThreadLocals和inheritableThreadLocals 是再Thread類出現(xiàn)之后才加的舵变。看注釋也驗(yàn)證了我這一點(diǎn)瘦穆。因?yàn)槲矣X得這樣設(shè)計(jì)有點(diǎn)破壞Thread類了纪隙。怎么說?InheritableThreadLocal 和 ThreadLocal 操作的 2個(gè)變量都是Thread類里面的扛或,InheritableThreadLocal 和 ThreadLocal 本質(zhì)只是操作那2個(gè)成員變量的工具而已绵咱。
-
ThreadLocal
ThreadLocal 比較簡單,就是 set的時(shí)候熙兔,獲取到當(dāng)前線程的 threadLocals 變量麸拄,然后存取數(shù)據(jù)
-
InheritableThreadLocal
InheritableThreadLocal 僅僅是在線程初始化的時(shí)候,繼承了一下父線程的 inheritableThreadLocals 變量黔姜,從而達(dá)到父子線程傳遞的目的拢切。
大致流程如下:
void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }
inheritableThreadLocals 如何 做到在父線程和子線程直接傳遞?
在Thread 創(chuàng)建的時(shí)候秆吵,就把父線程的inheritableThreadLocals 賦值到當(dāng)前線程了淮椰。
關(guān)鍵代碼(Thread.java 的 418行):
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
為什么線程池中讀取不到?
線程池的線程已經(jīng)脫離了外層線程的體系纳寂,沒有什么關(guān)聯(lián)主穗。但是我不知道為什么,毙芜,并發(fā)量非常小得情況忽媒,,可以讀取到腋粥。...晦雨。架曹。。我之后有時(shí)間再更新吧
疑問
目前發(fā)現(xiàn)在并發(fā)量不高的情況下闹瞧,線程池會復(fù)用原有線程绑雄。但是并發(fā)一上去,線程池的線程就都是全新的奥邮,和原來的沒有關(guān)系
總結(jié)
我之后有時(shí)間再更新吧