java開(kāi)發(fā)工作中對(duì)InheritableThreadLocal使用思考
引言
最近在工作中結(jié)合線程池使用 InheritableThreadLocal 出現(xiàn)了獲取線程變量“錯(cuò)誤”的問(wèn)題,看了相關(guān)的文檔和源碼后在此記錄。
1. 先說(shuō)結(jié)論
InheritableThreadLocal 只有在父線程創(chuàng)建子線程時(shí),在子線程中才能獲取到父線程中的線程變量;
當(dāng)配合線程池使用時(shí):“第一次在線程池中開(kāi)啟線程,能在子線程中獲取到父線程的線程變量,而當(dāng)該子線程開(kāi)啟之后,發(fā)生線程復(fù)用,該子線程仍然保留的是之前開(kāi)啟它的父線程的線程變量,而無(wú)法獲取當(dāng)前父線程中新的線程變量”,所以會(huì)發(fā)生獲取線程變量錯(cuò)誤的情況。
2. 實(shí)驗(yàn)例子
- 創(chuàng)建一個(gè)線程數(shù)固定為1的線程池,先在main線程中存入變量1,并使用線程池開(kāi)啟新的線程打印輸出線程變量,之后更改main線程的線程變量為變量2,再使用線程池中線程(發(fā)生線程復(fù)用)打印輸出線程變量,對(duì)比兩次輸出的值是否不同
/**
* 測(cè)試線程池下InheritableThreadLocal線程變量失效的場(chǎng)景
*/
public class TestInheritableThreadLocal {
private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
// 固定大小的線程池,保證線程復(fù)用
private static final ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
threadLocal.set("main線程 變量1");
// 正常取到 main線程 變量1
executorService.execute(() -> System.out.println(threadLocal.get()));
threadLocal.set("main線程 變量2");
// 線程復(fù)用再取還是 main線程 變量1
executorService.execute(() -> System.out.println(threadLocal.get()));
}
}
輸出結(jié)果:
main線程 變量1 main線程 變量1
發(fā)現(xiàn)兩次輸出結(jié)果值相同,證明發(fā)生線程復(fù)用時(shí),子線程獲取父線程變量失效
3. 詳解
3.1 JavaDoc
This class extends ThreadLocal to provide inheritance of values from parent thread to child thread: when a child thread is created, the child receives initial values for all inheritable thread-local variables for which the parent has values. Normally the child's values will be identical to the parent's; however, the child's value can be made an arbitrary function of the parent's by overriding the childValue method in this class. Inheritable thread-local variables are used in preference to ordinary thread-local variables when the per-thread-attribute being maintained in the variable (e.g., User ID, Transaction ID) must be automatically transmitted to any child threads that are created.
InheritableThreadLocal 繼承了 ThreadLocal, 以能夠讓子線程能夠從父線程中繼承線程變量: 當(dāng)一個(gè)子線程被創(chuàng)建時(shí),它會(huì)接收到父線程中所有可繼承的變量。通常情況下,子線程和父線程中的線程變量是完全相同的,但是可以通過(guò)重寫 childValue 方法來(lái)使父子線程中的值不同。
當(dāng)線程中維護(hù)的變量如UserId, TransactionId 等必須自動(dòng)傳遞到新創(chuàng)建的任何子線程時(shí),使用InheritableThreadLocal要優(yōu)于ThreadLocal
3.2 源碼
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 當(dāng)子線程被創(chuàng)建時(shí),通過(guò)該方法來(lái)初始化子線程中線程變量的值,
* 這個(gè)方法在父線程中被調(diào)用,并且在子線程開(kāi)啟之前。
*
* 通過(guò)重寫這個(gè)方法可以改變從父線程中繼承過(guò)來(lái)的值。
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
其中childValue方法來(lái)獲取父線程中的線程變量的值,也可通過(guò)重寫這個(gè)方法來(lái)將獲取到的線程變量的值進(jìn)行修改。
在getMap方法和createMap方法中,可以發(fā)現(xiàn)inheritableThreadLocals變量,它是 ThreadLocalMap,在Thread類中

3.2.1 childValue方法
- 開(kāi)啟新線程時(shí),會(huì)調(diào)用Thread的構(gòu)造方法
public Thread(ThreadGroup group, String name) {
init(group, null, name, 0);
}
- 沿著構(gòu)造方法向下,找到
init方法的最終實(shí)現(xiàn),其中有如下邏輯:為當(dāng)前線程創(chuàng)建線程變量以繼承父線程中的線程變量
/**
* @param inheritThreadLocals 為ture,代表是為 包含可繼承的線程變量 的線程進(jìn)行初始化
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 注意這里創(chuàng)建子線程的線程變量
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)創(chuàng)建子線程 InheritedMap 的具體實(shí)現(xiàn)
createInheritedMap 方法,最終會(huì)調(diào)用到 ThreadLocalMap 的私有構(gòu)造方法,傳入的參數(shù)parentMap即為父線程中保存的線程變量
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 注意?。?! 這里調(diào)用了childValue方法
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
這個(gè)方法會(huì)對(duì)父線程中的線程變量做深拷貝,其中調(diào)用了childValue方法來(lái)獲取/初始化子線程中的值,并保存到子線程中
- 由上可見(jiàn),可繼承的線程變量只是在線程被創(chuàng)建的時(shí)候進(jìn)行了初始化工作,這也就能解釋為什么在線程池中發(fā)生線程復(fù)用時(shí)不能獲取到父線程線程變量的原因
4. 實(shí)驗(yàn)例子流程圖

- main線程set main線程 變量1時(shí),會(huì)調(diào)用到
InheritableThreadLocal的createMap方法,創(chuàng)建inheritableThreadLocals并保存線程變量 - 開(kāi)啟子線程1時(shí),會(huì)深拷貝父線程中的線程變量到子線程中,如圖示
- main線程set main線程 變量2,會(huì)覆蓋主線程中之前set的mian線程變量1
- 最后發(fā)生線程復(fù)用,子線程1無(wú)法獲取到main線程新set的值,仍然打印 main線程 變量1
以上就是java開(kāi)發(fā)工作中對(duì)InheritableThreadLocal使用思考的詳細(xì)內(nèi)容,更多關(guān)于java開(kāi)發(fā)InheritableThreadLocal的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
超簡(jiǎn)潔java實(shí)現(xiàn)雙色球若干注隨機(jī)號(hào)碼生成(實(shí)例代碼)
這篇文章主要介紹了超簡(jiǎn)潔java實(shí)現(xiàn)雙色球若干注隨機(jī)號(hào)碼生成(實(shí)例代碼),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
MAC 系統(tǒng)如何使用 Sublime Text 2 直接編譯運(yùn)行 java 代碼
這篇文章主要介紹了MAC 系統(tǒng)如何使用 Sublime Text 2 直接編譯運(yùn)行 java 代碼,需要的朋友可以參考下2014-10-10
idea使用easyCode生成代碼(根據(jù)mybatis-plus模板創(chuàng)建自己的模板)
本文主要介紹了idea使用easyCode生成代碼,easyCode代碼生成器可以減少低價(jià)值搬磚,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10
java開(kāi)發(fā)之MD5加密算法的實(shí)現(xiàn)
本篇文章介紹了,java開(kāi)發(fā)之MD5加密算法的實(shí)現(xiàn)。需要的朋友參考下2013-05-05
Java中StringBuilder與StringBuffer的區(qū)別
在Java編程中,字符串的拼接是一項(xiàng)常見(jiàn)的操作。為了有效地處理字符串的拼接需求,Java提供了兩個(gè)主要的類:StringBuilder和StringBuffer,本文主要介紹了Java中StringBuilder與StringBuffer的區(qū)別,感興趣的可以了解一下2023-08-08
Java中l(wèi)ength,length(),size()詳解及區(qū)別
這篇文章主要介紹了Java中l(wèi)ength,length(),size()詳解及區(qū)別的相關(guān)資料,需要的朋友可以參考下2016-11-11

