線程封閉與ThreadLocal
多線程訪問(wèn)共享可變數(shù)據(jù)時(shí),涉及到線程間數(shù)據(jù)同步問(wèn)題。然而碴开,并不是所有時(shí)候都需要共享數(shù)據(jù),所以埂伦,線程封閉的概念就提出來(lái)了仔蝌。
通過(guò)將數(shù)據(jù)封閉在線程中而避免使用同步的技術(shù)稱為線程封閉泛领。
線程封閉的具體體現(xiàn)有:
- 局部變量
- ThreadLocal
局部變量
局部變量位于執(zhí)行線程的棧中,其他線程無(wú)法訪問(wèn)這個(gè)棧敛惊。線程封閉是局部變量的固有屬性渊鞋。
ThreadLocal
java.lang.ThreadLocal
,顧名思義豆混,它可以存放線程本地變量篓像。ThreadLocal
讓每個(gè)線程維護(hù)變量的一個(gè)副本,各線程通過(guò)ThreadLocal
去訪問(wèn)該變量時(shí)會(huì)拿到各自的副本皿伺,副本之間相互獨(dú)立员辩,互不影響,這樣競(jìng)爭(zhēng)條件被徹底消除了鸵鸥。
使用示例
下面通過(guò)一個(gè)例子來(lái)驗(yàn)證ThreadLocal
的特性奠滑。
public class ThreadLocalTest {
private static final ThreadLocal<String> value = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
value.set("主線程設(shè)置的123");
System.out.println("線程1執(zhí)行之前,主線程取到的值: " + value.get());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("線程1取到的值: " + value.get());
value.set("線程1設(shè)置的值456");
System.out.println("重新設(shè)置后線程1取到的值: " + value.get());
System.out.println("線程1執(zhí)行結(jié)束");
} finally {
value.remove();
}
}
}, "線程1");
thread.start();
// 等待線程1執(zhí)行結(jié)束
thread.join();
System.out.println("線程1執(zhí)行之后妒穴,主線程取到的值: " + value.get());
value.remove();
}
}
這段程序的輸出是:
線程1執(zhí)行之前宋税,主線程取到的值: 主線程設(shè)置的123
線程1取到的值: null
重新設(shè)置后線程1取到的值: 線程1設(shè)置的值456
線程1執(zhí)行結(jié)束
線程1執(zhí)行之后,主線程取到的值: 主線程設(shè)置的123
可以看出讼油,不同的線程通過(guò)ThreadLocal
進(jìn)行變量的讀寫時(shí)杰赛,是互不干擾的。
原理分析
ThreadLocal
這么神奇矮台,它到底是怎么實(shí)現(xiàn)的呢乏屯?
ThreadLocal
有3個(gè)核心方法:
get()
set()
remove()
這里主要看get()
方法 根时。
public T get() {
// 拿到當(dāng)前線程對(duì)應(yīng)的ThreadLocalMap對(duì)象
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 從map中查詢對(duì)應(yīng)的變量副本
if (map != null) {
// 以ThreadLocal對(duì)象為key,從map中獲取ThreadLocalMap.Entry對(duì)象
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果entry不為空辰晕,entry的value就是目標(biāo)變量副本
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否則蛤迎,初始化變量副本
return setInitialValue();
}
從get()
方法中可以看出,我們希望得到的變量副本存放在ThreadLocalMap
中含友。而ThreadLocalMap
是和線程綁定的:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在Thread
類里替裆,有這樣一個(gè)屬性:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap
的結(jié)構(gòu)如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap
是一個(gè)哈希表,它里面存放若干個(gè)指向ThreadLocal
對(duì)象的弱引用窘问,而我們需要的value值就掛靠在這個(gè)弱引用上辆童。因此,根據(jù)ThreadLocal
找到對(duì)應(yīng)的Entry
就能拿到目標(biāo)變量的副本南缓。
這里使用弱引用的目的是希望在
ThreadLocal
對(duì)象被回收后可以自動(dòng)回收value對(duì)象胸遇。
接下來(lái)看get()
方法里的第二個(gè)分支,setInitialValue()
汉形。進(jìn)入這個(gè)分支說(shuō)明當(dāng)前線程對(duì)應(yīng)的ThreadLocalMap
還未初始化纸镊,或者ThreadLocalMap
里面還沒(méi)有初始化ThreadLocal
對(duì)象對(duì)應(yīng)的Entry
。
private T setInitialValue() {
// 獲取初始值(變量副本)
T value = initialValue();
// 獲取當(dāng)前線程對(duì)應(yīng)的ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果ThreadLocalMap已經(jīng)初始化概疆,則將ThreadLocal對(duì)象和變量副本的映射關(guān)系保存在map中
if (map != null)
map.set(this, value);
// 否則逗威,初始化ThreadLocalMap,并保存ThreadLocal對(duì)象和變量副本的映射關(guān)系
else
createMap(t, value);
// 返回變量副本的值
return value;
}
其中岔冀,initialValue()
的實(shí)現(xiàn)是:
protected T initialValue() {
return null;
}
這是一個(gè)protected
方法凯旭,默認(rèn)返回null
值。這意味著使套,對(duì)于一個(gè)ThreadLocal
對(duì)象罐呼,線程訪問(wèn)它拿到的默認(rèn)變量副本是null
(這也解釋了在前面的示例中線程1一開(kāi)始拿到的是null
值)。我們可以覆蓋這個(gè)方法侦高,指定一個(gè)默認(rèn)的變量副本嫉柴,這樣可以省去調(diào)用get()
方法時(shí)的一次非空判斷。ThreadLocal
類里有一個(gè)靜態(tài)內(nèi)部類SuppliedThreadLocal
奉呛,它已經(jīng)幫我們覆蓋了默認(rèn)的initialValue()
方法计螺,只需要使用ThreadLocal
的靜態(tài)方法ThreadLocal#withInitial
就可以在創(chuàng)建ThreadLocal
對(duì)象時(shí)輕松指定默認(rèn)值。
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
到這里瞧壮,我們對(duì)get()
方法的有了大致的了解:獲取當(dāng)前線程的ThreadLocalMap
對(duì)象登馒,在ThreadLocalMap
里以ThreadLocal
對(duì)象為Key查詢Entry
,Entry
對(duì)應(yīng)的value就是我們希望得到的變量副本咆槽。如果查找失敗陈轿,就初始化變量副本(還可能初始化ThreadLocalMap
),并存入ThreadLocalMap
里,再將變量副本返回給調(diào)用者济欢。
ThreadLocal
與使用它的Thread
緊密相連:
- 一個(gè)
Thread
有且僅有一個(gè)ThreadLocalMap
對(duì)象赠堵。 - 一個(gè)
ThreadLocalMap
對(duì)象存儲(chǔ)多個(gè)Entry
對(duì)象小渊。 - 一個(gè)
Entry
對(duì)象的key的弱引用指向一個(gè)ThreadLocal
對(duì)象法褥。 - 一個(gè)
ThreadLocal
對(duì)象被多個(gè)線程所共享。 -
ThreadLocal
對(duì)象不持有value酬屉,value由線程的Entry
對(duì)象持有半等。
了解了get()
的實(shí)現(xiàn)邏輯,set()
和remove()
方法就不難理解了呐萨,這里不再展開(kāi)杀饵。
注意事項(xiàng)
ThreadLocal
的主要問(wèn)題是會(huì)產(chǎn)生臟數(shù)據(jù)和內(nèi)存泄漏。這兩個(gè)問(wèn)題通常是在線程池中使用ThreadLocal
引發(fā)的谬擦,因?yàn)榫€程池有線程復(fù)用和內(nèi)存常駐兩個(gè)特點(diǎn)切距。
- 臟數(shù)據(jù)
線程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù)。由于線程池會(huì)重用Thread
對(duì)象惨远,那么與Thread
綁定的ThreadLocalMap
變量也會(huì)被重用谜悟。如果在實(shí)現(xiàn)的線程的run()
方法中不顯式的調(diào)用remove()
清理與線程相關(guān)的ThreadLocal
信息,那么倘若下一個(gè)任務(wù)不調(diào)用set()
設(shè)置初始值北秽,就有可能get()
到重用的線程信息葡幸,包括ThreadLocal
所關(guān)聯(lián)的線程對(duì)象的value值。
- 內(nèi)存泄漏
通常使用static
關(guān)鍵字來(lái)修飾ThreadLocal
贺氓,在此場(chǎng)景下蔚叨,寄希望于ThreadLocal
對(duì)象失去引用后,觸發(fā)弱引用機(jī)制來(lái)回收Entry
的value就不現(xiàn)實(shí)了辙培。如果不進(jìn)行remove()
操作蔑水,那么ThreadLocal
對(duì)象持有的value是不會(huì)被釋放的。
以上兩個(gè)問(wèn)題解決辦法很簡(jiǎn)單扬蕊,就是在每次用完ThreadLocal
時(shí)搀别,必須及時(shí)調(diào)用remove()
方法清理。