一诗祸、前言
日常工作中,經(jīng)常使用ThreadLocal
來避免線程并發(fā)問題轴总,每個(gè)線程訪問自己的本地變量直颅,沒有競(jìng)爭(zhēng),沒有鎖肘习,非常高效〖食耍現(xiàn)在有一個(gè)業(yè)務(wù)場(chǎng)景,需要?jiǎng)?chuàng)建一些子線程來執(zhí)行任務(wù)漂佩,父線程中設(shè)置了ThreadLocal
的值,想在子線程中獲取罪塔,能獲取到嗎投蝉?答案是:不能。
了解ThreadLocal
的原理征堪,這個(gè)問題就很弱智瘩缆,用腳后跟想,父線程中set
佃蚜,那么這個(gè)存放值的ThreadLocalMap
就在父線程內(nèi)庸娱,子線程的threadLocals
是個(gè)null
,怎么可能從子線程get到父線程set的值呢谐算?
但是需求就要這樣熟尉,該如何實(shí)現(xiàn)?將父線程的ThreadLocalMap
復(fù)制一份給子線程洲脂?沒錯(cuò)斤儿,java
官方也是這么想的!
二、InheritableThreadLocal
1往果、使用方式
java 官方提供了一個(gè)類InheritableThreadLocal
疆液,使用方式上和ThreadLocal
完全一樣,就是類名不一樣陕贮。將ThreadLocal
替換為InheritableThreadLocal
堕油,就可以從子線程get
到父線程set
的值了。
2肮之、繼承關(guān)系
InheritableThreadLocal
是如何做到的呢掉缺?源碼底下見真知:
package java.lang;
/**
* @author Josh Bloch and Doug Lea
* @see ThreadLocal
* @since 1.2
*/
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
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);
}
}
InheritableThreadLocal
繼承自ThreadLocal
,重寫了三個(gè)方法childValue
局骤、getMap
攀圈、createMap
,用到Thread
的一個(gè)變量inheritableThreadLocals
峦甩。那就是InheritableThreadLocal
初始化的ThreadLocalMap
賦值給t.inheritableThreadLocals
赘来,set
和get
也是操作t.inheritableThreadLocals
。
public class Thread implements Runnable {
... ...
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
... ...
}
3凯傲、復(fù)制原理
那是如何將父線程的map復(fù)制給子線程的呢犬辰?
追溯到Thread
初始化,會(huì)調(diào)用一個(gè)init()
冰单,init
初始化的東西較多幌缝,直接看重點(diǎn):
真相了,創(chuàng)建子線程時(shí)诫欠,默認(rèn)inheritThreadLocals=true
涵卵,父線程即當(dāng)前線程的inheritableThreadLocals!=null
,則將父線程的inheritableThreadLocals
復(fù)制給子線程荒叼。
// java.lang.ThreadLocal#createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
//java.lang.ThreadLocal.ThreadLocalMap#ThreadLocalMap
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 遍歷復(fù)制
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) {
// InheritableThreadLocal重寫了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++;
}
}
}
}
三轿偎、childValue的用意
ThreadLocal
中childValue
沒有給出具體實(shí)現(xiàn),而在InheritableThreadLocal
中也只是簡單實(shí)現(xiàn)了下被廓。
//java.lang.ThreadLocal#childValue
T childValue(T parentValue) {
throw new UnsupportedOperationException();
}
//java.lang.InheritableThreadLocal#childValue
protected T childValue(T parentValue) {
return parentValue;
}
從父線程復(fù)制ThreaLocalMap
到子線程時(shí)坏晦,值從childValue
函數(shù)過了一遍再賦值給Entry
,是何意圖嫁乘?關(guān)鍵是InheritableThreadLocal
也沒做什么昆婿,但是不難猜出,ThreadLocal
留了一個(gè)childValue
就是讓InheritableThreadLocal
實(shí)現(xiàn)的蜓斧,雖然InheritableThreadLocal
沒做什么仓蛆,但是使用者可以繼承InheritableThreadLocal
重寫childValue
,對(duì)value
做特殊處理法精。為什么可能要對(duì)value
做特殊處理呢多律?
比如痴突,設(shè)置的值是一個(gè)自定義的引用類型,那么從父線程復(fù)制到多個(gè)子線程的值就存在并發(fā)問題(值傳遞狼荞,地址值是共享的)辽装,所以復(fù)制的時(shí)候要保證復(fù)制給每個(gè)子線程的地址值不一樣,繼承InheritableThreadLocal
實(shí)現(xiàn)childValue
的深拷貝相味,定制化一個(gè)自己的InheritableThreadLocal
:
public class MyInheritableThreadLocal<T> extends InheritableThreadLocal<T>{
protected T childValue(T parentValue) {
System.out.println("MyInheritableThreadLocal拾积。。丰涉。");
// 深拷貝
Gson gson = new Gson();
String s = gson.toJson(parentValue);
return (T)gson.fromJson(s, parentValue.getClass());
}
}
public class InheritableThreadLocalTest {
public static void main(String[] args) throws InterruptedException {
InheritableThreadLocal<Stu> itl = new MyInheritableThreadLocal<Stu>();
itl.set(new Stu());
System.out.println("父線程:" + itl.get().toString());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程1:" + itl.get().toString());
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子線程2:" + itl.get().toString());
}
});
thread2.start();
}
static class Stu {
private String name = "xxx";
}
}
// 控制臺(tái)打印
父線程:com.stefan.DailyTest.InheritableThreadLocalTest$Stu@49476842
MyInheritableThreadLocal拓巧。。一死。
子線程1:com.stefan.DailyTest.InheritableThreadLocalTest$Stu@7446b2ac
MyInheritableThreadLocal肛度。。投慈。
子線程2:com.stefan.DailyTest.InheritableThreadLocalTest$Stu@75f4c190
四承耿、總結(jié)
-
InheritableThreadLocal
可以實(shí)現(xiàn)子線程獲取父線程的本地變量。 - 子線程初始化時(shí)伪煤,若父線程(當(dāng)前線程)的本地變量
inheritableThreadLocals
不為null加袋,則復(fù)制給子線程。 -
ThreadLocal
留個(gè)childValue
的用意抱既,就是讓InheritableThreadLocal
實(shí)現(xiàn)职烧,并且可以讓客戶端自定義重寫childValue
對(duì)從父線程復(fù)制到子線程的值做特殊處理。 - 若父線程使用
InheritableThreadLocal
設(shè)置了自定義引用類型的值防泵,復(fù)制給子線程時(shí)存在并發(fā)問題蚀之,需要自行實(shí)現(xiàn)childValue
的深拷貝。
拋個(gè)問題:
如果使用線程池創(chuàng)建子線程捷泞,子線程只會(huì)初始化一次恬总,父線程中使用InheritableThreadLocal
設(shè)置值,因?yàn)閺?fù)制機(jī)制是在線程初始化的時(shí)候肚邢,那么父線程只有在線程池初始化子線程時(shí)同步復(fù)制一次數(shù)據(jù),后續(xù)父線程再修改值拭卿,就無法同步更新到線程池中的子線程了骡湖,這該怎么辦呢?
只要在每次提交任務(wù)時(shí)復(fù)制就可以了峻厚,這就要對(duì)線程池以及InheritableThreadLocal
做一些定制化處理响蕴,讓復(fù)制機(jī)制放在每次提交任務(wù)的時(shí)候,阿里有一個(gè)開源項(xiàng)目給出了解決方案https://github.com/alibaba/transmittable-thread-local惠桃,后續(xù)可深入了解其實(shí)現(xiàn)原理浦夷。
PS: 如若文章中有錯(cuò)誤理解辖试,歡迎批評(píng)指正,同時(shí)非常期待你的評(píng)論劈狐、點(diǎn)贊和收藏罐孝。我是徐同學(xué),愿與你共同進(jìn)步肥缔!