簡書江溢Jonny谱邪,轉(zhuǎn)載請注明原創(chuàng)出處,謝謝庶诡!
本文內(nèi)容將基于JDK1.7的源碼進行討論惦银,并且在文章的結(jié)尾,筆者將會給出一些經(jīng)驗之談,希望能給學習者帶來些幫助扯俱。
一书蚪、線程封閉
在《Java并發(fā)編程實戰(zhàn)》一書中提到,“當訪問共享的可變數(shù)據(jù)時迅栅,通常需要使用同步殊校。一種避免使用同步的方式就是不共享數(shù)據(jù)”。因此提出了“線程封閉”的概念读存,一種經(jīng)常使用線程封閉的應用場景就是JDBC的Connection为流,通過線程封閉技術(shù),可以把鏈接對象封閉在某個線程內(nèi)部让簿,從而避免出現(xiàn)多個線程共享同一個鏈接的情況敬察。而線程封閉總共有三種類型的呈現(xiàn)形式:
1)Ad-hoc線程封閉。維護線程封閉性的職責由程序?qū)崿F(xiàn)來承擔拜英,然而這種實現(xiàn)方式是脆弱的静汤;
2)棧封閉。實際上通過盡量使用局部變量的方式居凶,避免其他線程獲取數(shù)據(jù)虫给;
3)ThreadLocal類。通過JDK提供的ThreadLocal類侠碧,可以保證某個對象僅在線程內(nèi)部被訪問抹估,而該類正是本篇文章將要討論的內(nèi)容。
二弄兜、誤區(qū)
網(wǎng)上很多人會想當然的認為药蜻,ThreadLocal的實現(xiàn)就是一個類似Map<Thread, T>的對象,其中對象中保存了特定某個線程的值替饿,然而實際上的實現(xiàn)并非如此语泽,筆者在這里將就著JDK 1.7的源碼對ThreadLocal的實現(xiàn)進行解讀,如果有不對的或者不理解的地方视卢,歡迎留言斧正踱卵。
三、舉個栗子
SimpleDateFormat是JDK提供的据过,一類用于處理時間格式的工具惋砂,但是因為早期的實現(xiàn),導致這個類并非是一個線程安全的實現(xiàn)绳锅,因此西饵,在使用的時候我們會需要使用線程封閉技術(shù)來保證使用該類過程中的線程安全,在這里鳞芙,我們使用了ThreadLocal眷柔,下面的實現(xiàn)是使用SimpleDateFormat格式化當前時間并輸出:
private static ThreadLocal<SimpleDateFormat> localFormatter =
new ThreadLocal<SimpleDateFormat>();
static {
localFormatter.set(new SimpleDateFormat("yyyyMMdd"));
}
public static void main(String[] args) {
Date now = new Date();
System.out.println(localFormatter.get().format(now));
}
四期虾、系統(tǒng)設(shè)計
在JDK 1.7中,ThreadLocal是一個如下圖所示的設(shè)計:
可以在圖里看到驯嘱,每個線程內(nèi)部都持有一個ThreadLocal.ThreadLocalMap類型的對象彻消,但是該對象只能被ThreadLocal類處理。那么讀者暫時可以理解成宙拉,每個線程的內(nèi)部都持有了一個類似Map<ThreadLocal, T>結(jié)構(gòu)的表(實際上,Map的維護的鍵值對丙笋,是一個WeakReference的弱引用結(jié)構(gòu)谢澈,這個比SoftReference還要弱一點)。
為什么這樣設(shè)計御板?
看到這里锥忿,有的讀者會產(chǎn)生這樣的提問,為什么是這樣的設(shè)計怠肋?好問題敬鬓,按照很多的人的想法里,應該有兩種設(shè)計方式:
1)全局ConcurrentMap<Thread笙各,T>結(jié)構(gòu)钉答。該設(shè)計在對應的ThreadLocal對象內(nèi)維持一個本地變量表,以當前線程(使用Thread.currentThread()方法)作為key杈抢,查找對應的的本地變量(value值)数尿,那么這么設(shè)計存在什么問題呢?
第一惶楼,全局的ConcurrentMap<Thread, T>表右蹦,這類數(shù)據(jù)結(jié)構(gòu)雖然是一類分段式且線程安全的容器,但是這類容器仍然會有線程同步的的額外開銷歼捐;
第二何陆,隨著線程的銷毀,原有的ConcurrentMap<Thread, T>沒有被回收豹储,因此導致了內(nèi)存泄露贷盲;
2)局部HashMap<ThreadLocal, T>的結(jié)構(gòu)。在該設(shè)計下颂翼,每個線程對象維護一個Map<ThreadLocal, T>晃洒,可以這樣仍然會存在一些問題:
比如某個線程執(zhí)行時間非常長,然而在此過程中朦乏,某個對象已經(jīng)不可達(理論上可以被GC)球及,但是由于HashMap<ThreadLocal, T>數(shù)據(jù)結(jié)構(gòu)的存在,仍然有對象被當前線程強引用呻疹,從而導致了該對象不能被GC吃引,因此同樣也會導致內(nèi)存泄露吗伤。
五、源碼實現(xiàn)
在闡述完ThreadLocal設(shè)計以后瞻鹏,我們一起來看看JDK1.7 是怎么實現(xiàn)ThreadLocal的尖滚。
ThreadLocal類的本身實現(xiàn)比較簡單,其代碼的核心和精髓實際都在它的內(nèi)部靜態(tài)類ThreadLocalMap中庐氮,因此這里我們不再贅述ThreadLocal類的各種接口方法语稠,直接進入主題,一起來研究ThreadLocalMap類相關(guān)的源碼弄砍。
首先我們翻閱Thread類的源碼仙畦,可以看到這么一句:
public
class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null; // 注意這里
...
}
可以看到在每個Thread類的內(nèi)部,都耦合了一個ThreadLocalMap類型的引用音婶,由于ThreadLocalMap類是ThreadLocal類的私有內(nèi)嵌類慨畸,因此ThreadLocalMap類型的對象只能由ThreadLocal類打理:
public class ThreadLocal<T> {
...
// 內(nèi)部私有靜態(tài)類
static class ThreadLocalMap {
...
}
...
}
關(guān)于ThreadLocalMap類實現(xiàn),我們也可以把它理解成是一類哈希表衣式,那么作為哈希表寸士,就要包含:數(shù)據(jù)結(jié)構(gòu)、尋址方式碴卧、哈希表擴容(Rehash)弱卡,除了哈希表的部分外,ThreadLocalMap還包含了“垃圾回收”的過程住册。因此谐宙,我們將按以上模塊分別介紹ThreadLocalMap類的實現(xiàn)。
1. 數(shù)據(jù)結(jié)構(gòu)
那么接下來我們看看ThreadLocalMap中數(shù)據(jù)結(jié)構(gòu)的定義:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
Object value; // 實際保存的值
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
/**
* 哈希表初始大小界弧,但是這個值無論怎么變化都必須是2的N次方
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 哈希表中實際存放對象的容器凡蜻,該容器的大小也必須是2的冪數(shù)倍
*/
private Entry[] table;
/**
* 表中Entry元素的數(shù)量
*/
private int size = 0;
/**
* 哈希表的擴容閾值
*/
private int threshold; // 默認值為0
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
...
/**
* 并不是Thread被創(chuàng)建后就一定會創(chuàng)建一個新的ThreadLocalMap,
* 除非當前Thread真的用了ThreadLocal
* 并且賦值到ThreadLocal后才會創(chuàng)建一個ThreadLocalMap
*/
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode
& (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
可以從上面看到這些信息:
1)存放對象信息的表是一個數(shù)組垢箕。這類方式和HashMap有點像划栓;
2)數(shù)組元素是一個WeakReference(弱引用)的實現(xiàn)。弱引用是一類比軟引用更加脆弱的類型(按照強弱程度分別為 強引用>軟引用 > 弱引用 > 虛引用)条获,至于為什么使用弱引用忠荞,這是因為線程的執(zhí)行時間可能很長,但是對應的ThreadLocal對象生成時間未必有線程的執(zhí)行壽命那般長帅掘,在對應ThreadLocal對象由該線程作為根節(jié)點出發(fā)委煤,邏輯上不可達時,就應該可以被GC修档,如果使用了強引用碧绞,該對象無法被成功GC,因此會帶來內(nèi)存泄露的問題吱窝;
3)哈希表的大小必須是2的N次方讥邻。至于這部分迫靖,在后面會提到,實際上這個長度的設(shè)計和位運算有關(guān)兴使;
4)閾值threshold系宜。這個概念同樣和HashMap內(nèi)部實現(xiàn)的閾值類似,當數(shù)組長度到了某個閾值時发魄,為了減少散列函數(shù)的碰撞盹牧,不得不擴展容量大小励幼;
結(jié)構(gòu)如圖所示欢策,虛線部分表示的是一個弱引用:
2、尋址方式
首先我們根據(jù)getEntry()方法一起來觀察一下根據(jù)哈希算法尋址某個元素的過程赏淌,可以看到,這是一類“直接尋址法”的實現(xiàn):
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 尋址失敗啄清,需要繼續(xù)探察
return getEntryAfterMiss(key, i, e);
}
在這里我們注意到一個“key.threadLocalHashCode”對象六水,該對象的生成方式如下:
public class ThreadLocal<T> {
private final int threadLocalHashCode =
nextHashCode();
/**
* 計算哈希值相關(guān)的魔數(shù)
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回遞增后的哈希值
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
根據(jù)一個固定的值0x61c88647
(為什么是這個數(shù)字,我們稍后再提)辣卒,在每次生成新的ThreadLocal對象時遞增這個哈希值
之前已經(jīng)提到了掷贾,table的length必須滿足2的N次方,因此按照位運算"key.threadLocalHashCode & (table.length - 1)"獲得是哈希值的的末N位荣茫,根據(jù)這一哈希算法計算的結(jié)果取到哈希表中對應的元素想帅。可是這個時候啡莉,又會遇到哈希算法的經(jīng)典問題——哈希碰撞港准。
針對哈希碰撞,我們通常有三種手段:
1)拉鏈法咧欣。這類哈希碰撞的解決方法將所有關(guān)鍵字為同義詞的記錄存儲在同一線性鏈表中浅缸。JDK1.7已經(jīng)在HashMap類中實現(xiàn)了,感興趣的可以去看看魄咕;
2)再哈希法衩椒。當發(fā)生沖突時,使用第二個哮兰、第三個毛萌、哈希函數(shù)計算地址,直到無沖突時喝滞。缺點:計算時間增加阁将。比如第一次按照姓首字母進行哈希,如果產(chǎn)生沖突可以按照姓字母首字母第二位進行哈希右遭,再沖突冀痕,第三位荔睹,直到不沖突為止;
3)開放地址法(ThreadLocalMap使用的正是這類方法)言蛇。所謂的開放定址法就是一旦發(fā)生了沖突僻他,就去尋找下一個空的散列地址,只要散列表足夠大腊尚,空的散列地址總能找到吨拗,并將記錄存入。
那么我們一起來看看ThreadLocalMap的實現(xiàn)婿斥,我們通過getEntry()方法按照哈希函數(shù)取得哈希表中的值劝篷,在該方法內(nèi)部,我們將用到一個getEntryAfterMiss()方法:
/**
* 如果在getEntry方法中不能馬上找到對應的Entry民宿,將調(diào)用該方法
*
* @param e table[i]對應的entry值
*/
private Entry getEntryAfterMiss(
ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
// 對從該位置開始的的對象進行清理(開發(fā)者主動GC)
expungeStaleEntry(i);
else
// 查找下一個對象
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
在該方法中可以看到娇妓,當根據(jù)哈希函數(shù)直接查找對應的位置失敗后,就會從當前的位置往后開始尋找活鹰,直到找到對應的key值哈恰,另外,如果發(fā)現(xiàn)有key值已經(jīng)被GC了志群,那么相應的着绷,也應該啟動expungeStaleEntry()
方法,清理掉無效的Entry锌云。
類似的荠医,ThreadLocalMap類的set方法,也是按照 “根據(jù)哈希函數(shù)查找位置→ 如果查找不成功就沿著當前位置查找 → 如果發(fā)現(xiàn)垃圾數(shù)據(jù)及時清理” 的路徑進行著:
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
// 清理無效數(shù)據(jù)
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 清理無效數(shù)據(jù)后判斷是否仍需擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 擴容
}
該函數(shù)在“尋址方式”上和getEntry()方法類似桑涎,因此就不展開闡述了彬向。
為什么是0x61c88647
這個魔數(shù)的選取與斐波那契散列有關(guān),0x61c88647
對應的十進制為1640531527
攻冷。斐波那契散列的乘數(shù)可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))
可以得到2654435769
幢泼,如果把這個值給轉(zhuǎn)為帶符號的int,則會得到-1640531527
(也就是0x61c88647)讲衫。通過理論與實踐缕棵,當我們用0x61c88647作為魔數(shù)累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode
再與2的冪取模,得到的結(jié)果分布很均勻涉兽。ThreadLocalMap使用的是線性探測法招驴,均勻分布的好處在于很快就能探測到下一個臨近的可用slot,從而保證效率枷畏。
3别厘、哈希表擴容(Rehash)
我們一起來回憶一下,table對象的起始容量是可以容納16個對象拥诡,在set()方法的尾部可以看到以下內(nèi)容:
// 清理無效數(shù)據(jù)后判斷是否仍需擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash(); // 擴容
如果當前容量大小大于閾值(threshold)后触趴,將會發(fā)起一次擴容(rehash)操作氮发。
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
在該方法中,首先嘗試徹底清理表中的無效元素(失效的弱引用)冗懦,然后判斷當前是否仍然大于threshold值的3/4爽冕。
而threshold值,在文章開始的時候就已經(jīng)提起過披蕉,是當前容量大小的2/3:
/**
* 在當前容量大小超過table大小的2/3時可能會觸發(fā)一次rehash操作
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
那么我們一起看看resize()方法:
/**
* 成倍擴容table
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 直接倍增
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null; // 釋放無效的對象
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
在該方法內(nèi)部颈畸,首先創(chuàng)建一個新的表,表的大小是原來表大小的兩倍没讲,然后再逐個復制原表內(nèi)容到新表中眯娱,如果發(fā)現(xiàn)有無效對象,則把Entry對象中對應的value引用置為NULL爬凑,方便后面垃圾收集器對該對象的回收徙缴。
4、垃圾回收
此時筆者再次貼出引用的圖示:
可以看到Entry對象到ThreadLocal對象是一個弱引用的關(guān)系嘁信,而指向Object對象仍然是一個強引用的關(guān)系于样,因此,雖然由于弱引用的ThreadLocal對象隨著ROOT路徑不可達而被垃圾收集器清理后吱抚,但是仍然殘留有Object對象,不及時清理會存在“內(nèi)存泄露”的問題考廉。
那么我們看看和垃圾收集有關(guān)的方法:
/**
* 該方法將在set方法中被調(diào)用秘豹,在set某個值時,通過散列函數(shù)指向某個位置昌粤,然而
* 此時該位置上存在一個垃圾Entry既绕,將會嘗試使用此方法用新值覆蓋舊值,不過該方
* 法還承擔了“主動垃圾回收”的功能涮坐。
*
* @param key 以ThreadLocal類對象作為key
* @param value 通過ThreadLocal類對象找到對應的值
*/
private void replaceStaleEntry(
ThreadLocal key, Object value,int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前掃描凄贩,查找最前的一個無效slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 向后遍歷table,直到當前表中所指的位置是一個空值或
// 者已經(jīng)找到了和ThreadLocal對象匹配的值
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
// 之前設(shè)置新值時袱讹,如果當前哈希位存在沖突疲扎,
// 那么就要順延到后面空的slot中存放。
// 既然當前哈希位原來對應的ThreadLocal對象已經(jīng)
// 被回收了捷雕,那么被順延放置的ThreadLocal對象
// 自然就要被向前調(diào)整到當前位置中去
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e; // swap操作
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 清理一波無效slot
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return; // 找到了就直接返回
}
// 如果當前的slot已經(jīng)無效椒丧,并且向前掃描過程中沒有無效slot,
// 則更新slotToExpunge為當前位置
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// key沒找到就原地創(chuàng)建一個新的
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 在探測過程中如果發(fā)現(xiàn)任何無效slot救巷,
// 則做一次清理(連續(xù)段清理+啟發(fā)式清理)
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 這個函數(shù)做了兩件事情:
* 1)清理當前無效slot(由staleSlot指定位置)
* 2)從staleSlot開始壶熏,一直到null位,清理掉中間所有的無效slot
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理當前無效slot(由staleSlot指定位置)
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 從staleSlot開始浦译,一直到null位棒假,清理掉中間所有的無效slot
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
// 清理掉無效slot
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
// 當前ThreadLocal不在它計算出來的哈希位上溯职,
// 說明之前在插入的時候被順延到哈希位后面放置了,
// 因此此時需要向前調(diào)整位置
if (h != i) {
tab[i] = null;
// 從計算出來的哈希位開始往后查找帽哑,找到一個適合它的空位
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* 啟發(fā)式地清理slot,
* n是用于控制控制掃描次數(shù)的
* 正常情況下如果log2(n)次掃描沒有發(fā)現(xiàn)無效slot谜酒,函數(shù)就結(jié)束了
* 但是如果發(fā)現(xiàn)了無效的slot,將n置為table的長度len祝拯,做一次連續(xù)段的清理
* 再從下一個空的slot開始繼續(xù)掃描
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
下面我來圖解一下expungeStaleEntry方法的流程:
以上是ThreadLocal源碼介紹的全部內(nèi)容甚带。下面筆者將補充一些在實際開發(fā)過程中遇到的問題,作為補充信息一并分享佳头。
六鹰贵、經(jīng)驗之談
1、謹慎在ThreadExecutorPool中使用ThreadLocal
在ThreadExecutorPool中康嘉,Thread是復用的碉输,因此每個Thread對應的ThreadLocal空間也是被復用的,如果開發(fā)者不希望ThreadExecutorPool中的下一個Task能讀取到上一個Task在ThreadLocal中存入的信息亭珍,那就不應該使用ThreadLocal敷钾。
舉個例子:
final ThreadLocal<String> threadLocal =
new ThreadLocal<String>();
// 線程池大小為1
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>());
// 任務1
threadPoolExecutor.execute(new Runnable() {
public void run() {
threadLocal.set("first insert");
}
});
// 任務2
threadPoolExecutor.execute(new Runnable() {
public void run() {
System.out.println(threadLocal.get());
}
});
像這樣,第二個任務能讀取到第一個任務插入的數(shù)據(jù)肄梨。但是如果此時線程池中任務一拋出一個異常出來:
final ThreadLocal<String> threadLocal =
new ThreadLocal<String>();
// 線程池大小為1
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>());
// 任務1
threadPoolExecutor.execute(new Runnable() {
public void run() {
threadLocal.set("first insert");
// 拋一個異常
throw new RuntimeException("throw a exception");
}
});
// 任務2
threadPoolExecutor.execute(new Runnable() {
public void run() {
System.out.println(threadLocal.get());
}
});
那么此時阻荒,第二個任務無法讀取到第一個任務插入的數(shù)據(jù)(因為第一個線程因為拋異常已經(jīng)死了,任務二用的是新線程執(zhí)行)
2众羡、不要濫用ThreadLocal
很多開發(fā)者為了能夠在類和類直接傳輸數(shù)據(jù)侨赡,而不想把方法里的參數(shù)表寫得過于龐大,那么可能會帶來類于類直接重度耦合的問題粱侣,這樣不利于后面的開發(fā)羊壹。
3、要先set才能get
繼續(xù)舉個例子:
public class TestMain {
public ThreadLocal<Integer> intThreadLocal =
new ThreadLocal<Integer>();
public int getCount() {
return intThreadLocal.get();
}
public static void main(String[] args) {
System.out.println(new TestMain().getCount());
}
}
在這里齐婴,沒有先set就直接get油猫,將會拋出一個NullPointerException,原因我們一起來回顧一下ThreadLocal的代碼:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue(); // 返回了NULL導致NPE
}
private T setInitialValue() {
T value = initialValue(); // 這里返回了NULL
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
以上就是“線程封閉之ThreadLocal源碼詳解”的全部內(nèi)容了柠偶,如果還想進一步的交流情妖,歡迎關(guān)注我的微信公眾號“Jonny的日知錄”~:-D