原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑
詳情請(qǐng)戳www.codercc.com
1. 造成內(nèi)存泄漏的原因?
threadLocal是為了解決對(duì)象不能被多線程共享訪問的問題鸯绿,通過threadLocal.set方法將對(duì)象實(shí)例保存在每個(gè)線程自己所擁有的threadLocalMap中满粗,這樣每個(gè)線程使用自己的對(duì)象實(shí)例,彼此不會(huì)影響達(dá)到隔離的作用晒喷,從而就解決了對(duì)象在被共享訪問帶來線程安全問題孝偎。如果將同步機(jī)制和threadLocal做一個(gè)橫向比較的話,同步機(jī)制就是通過控制線程訪問共享對(duì)象的順序凉敲,而threadLocal就是為每一個(gè)線程分配一個(gè)該對(duì)象衣盾,各用各的互不影響。打個(gè)比方說爷抓,現(xiàn)在有100個(gè)同學(xué)需要填寫一張表格但是只有一支筆势决,同步就相當(dāng)于A使用完這支筆后給B,B使用后給C用......老師就控制著這支筆的使用順序废赞,使得同學(xué)之間不會(huì)產(chǎn)生沖突徽龟。而threadLocal就相當(dāng)于,老師直接準(zhǔn)備了100支筆唉地,這樣每個(gè)同學(xué)都使用自己的据悔,同學(xué)之間就不會(huì)產(chǎn)生沖突。很顯然這就是兩種不同的思路耘沼,同步機(jī)制以“時(shí)間換空間”极颓,由于每個(gè)線程在同一時(shí)刻共享對(duì)象只能被一個(gè)線程訪問造成整體上響應(yīng)時(shí)間增加,但是對(duì)象只占有一份內(nèi)存群嗤,犧牲了時(shí)間效率換來了空間效率即“時(shí)間換空間”菠隆。而threadLocal,為每個(gè)線程都分配了一份對(duì)象狂秘,自然而然內(nèi)存使用率增加骇径,每個(gè)線程各用各的,整體上時(shí)間效率要增加很多者春,犧牲了空間效率換來時(shí)間效率即“空間換時(shí)間”破衔。
關(guān)于threadLocal,threadLocalMap更多的細(xì)節(jié)可以看這篇文章,給出了很詳細(xì)的各個(gè)方面的知識(shí)(很多也是面試高頻考點(diǎn))钱烟。threadLocal,threadLocalMap,entry之間的關(guān)系如下圖所示:
上圖中晰筛,實(shí)線代表強(qiáng)引用,虛線代表的是弱引用拴袭,如果threadLocal外部強(qiáng)引用被置為null(threadLocalInstance=null)的話读第,threadLocal實(shí)例就沒有一條引用鏈路可達(dá),很顯然在gc(垃圾回收)的時(shí)候勢必會(huì)被回收拥刻,因此entry就存在key為null的情況怜瞒,無法通過一個(gè)Key為null去訪問到該entry的value。同時(shí)般哼,就存在了這樣一條引用鏈:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,導(dǎo)致在垃圾回收的時(shí)候進(jìn)行可達(dá)性分析的時(shí)候,value可達(dá)從而不會(huì)被回收掉盼砍,但是該value永遠(yuǎn)不能被訪問到尘吗,這樣就存在了內(nèi)存泄漏。當(dāng)然浇坐,如果線程執(zhí)行結(jié)束后睬捶,threadLocal,threadRef會(huì)斷掉近刘,因此threadLocal,threadLocalMap擒贸,entry都會(huì)被回收掉【蹩剩可是介劫,在實(shí)際使用中我們都是會(huì)用線程池去維護(hù)我們的線程,比如在Executors.newFixedThreadPool()時(shí)創(chuàng)建線程的時(shí)候案淋,為了復(fù)用線程是不會(huì)結(jié)束的座韵,所以threadLocal內(nèi)存泄漏就值得我們關(guān)注。
2. 已經(jīng)做出了哪些改進(jìn)踢京?
實(shí)際上誉碴,為了解決threadLocal潛在的內(nèi)存泄漏的問題,Josh Bloch and Doug Lea大師已經(jīng)做了一些改進(jìn)瓣距。在threadLocal的set和get方法中都有相應(yīng)的處理黔帕。下文為了敘述,針對(duì)key為null的entry蹈丸,源碼注釋為stale entry成黄,直譯為不新鮮的entry,這里我就稱之為“臟entry”逻杖。比如在ThreadLocalMap的set方法中:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
在該方法中針對(duì)臟entry做了這樣的處理:
- 如果當(dāng)前table[i]奋岁!=null的話說明hash沖突就需要向后環(huán)形查找,若在查找過程中遇到臟entry就通過replaceStaleEntry進(jìn)行處理荸百;
- 如果當(dāng)前table[i]==null的話說明新的entry可以直接插入闻伶,但是插入后會(huì)調(diào)用cleanSomeSlots方法檢測并清除臟entry
2.1 cleanSomeSlots
該方法的源碼為:
/* @param i a position known NOT to hold a stale entry. The
* scan starts at the element after i.
*
* @param n scan control: {@code log2(n)} cells are scanned,
* unless a stale entry is found, in which case
* {@code log2(table.length)-1} additional cells are scanned.
* When called from insertions, this parameter is the number
* of elements, but when from replaceStaleEntry, it is the
* table length. (Note: all this could be changed to be either
* more or less aggressive by weighting n instead of just
* using straight log n. But this version is simple, fast, and
* seems to work well.)
*
* @return true if any stale entries have been removed.
*/
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;
}
入?yún)ⅲ?/strong>
i表示:插入entry的位置i,很顯然在上述情況2(table[i]==null)中管搪,entry剛插入后該位置i很顯然不是臟entry;
-
參數(shù)n
2.1. n的用途
主要用于掃描控制(scan control),從while中是通過n來進(jìn)行條件判斷的說明n就是用來控制掃描趟數(shù)(循環(huán)次數(shù))的铡买。在掃描過程中更鲁,如果沒有遇到臟entry就整個(gè)掃描過程持續(xù)log2(n)次,log2(n)的得來是因?yàn)?code>n >>>= 1浸锨,每次n右移一位相當(dāng)于n除以2稠项。如果在掃描過程中遇到臟entry的話就會(huì)令n為當(dāng)前hash表的長度(
n=len
)褂始,再掃描log2(n)趟谓罗,注意此時(shí)n增加無非就是多增加了循環(huán)次數(shù)從而通過nextIndex往后搜索的范圍擴(kuò)大媒至,示意圖如下
按照n的初始值顶别,搜索范圍為黑線,當(dāng)遇到了臟entry拒啰,此時(shí)n變成了哈希數(shù)組的長度(n取值增大)驯绎,搜索范圍log2(n)增大,紅線表示谋旦。如果在整個(gè)搜索過程沒遇到臟entry的話剩失,搜索結(jié)束,采用這種方式的主要是用于時(shí)間效率上的平衡册着。
2.2. n的取值
如果是在set方法插入新的entry后調(diào)用(上述情況2)拴孤,n位當(dāng)前已經(jīng)插入的entry個(gè)數(shù)size;如果是在replaceSateleEntry方法中調(diào)用n為哈希表的長度len甲捏。
2.2 expungeStaleEntry
如果對(duì)輸入?yún)?shù)能夠理解的話演熟,那么cleanSomeSlots方法搜索基本上清除了,但是全部搞定還需要掌握expungeStaleEntry方法司顿,當(dāng)在搜索過程中遇到了臟entry的話就會(huì)調(diào)用該方法去清理掉臟entry芒粹。源碼為:
/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//清除當(dāng)前臟entry
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
//2.往后環(huán)形繼續(xù)查找,直到遇到table[i]==null時(shí)結(jié)束
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//3. 如果在向后搜索過程中再次遇到臟entry,同樣將其清理掉
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//處理rehash的情況
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
該方法邏輯請(qǐng)看注釋(第1,2,3步)免猾,主要做了這么幾件事情:
- 清理當(dāng)前臟entry是辕,即將其value引用置為null,并且將table[staleSlot]也置為null猎提。value置為null后該value域變?yōu)椴豢蛇_(dá)获三,在下一次gc的時(shí)候就會(huì)被回收掉,同時(shí)table[staleSlot]為null后以便于存放新的entry;
- 從當(dāng)前staleSlot位置向后環(huán)形(nextIndex)繼續(xù)搜索锨苏,直到遇到哈希桶(tab[i])為null的時(shí)候退出疙教;
- 若在搜索過程再次遇到臟entry,繼續(xù)將其清除伞租。
也就是說該方法贞谓,清理掉當(dāng)前臟entry后,并沒有閑下來繼續(xù)向后搜索葵诈,若再次遇到臟entry繼續(xù)將其清理裸弦,直到哈希桶(table[i])為null時(shí)退出。因此方法執(zhí)行完的結(jié)果為 從當(dāng)前臟entry(staleSlot)位到返回的i位作喘,這中間所有的entry不是臟entry理疙。為什么是遇到null退出呢?原因是存在臟entry的前提條件是 當(dāng)前哈希桶(table[i])不為null,只是該entry的key域?yàn)閚ull泞坦。如果遇到哈希桶為null,很顯然它連成為臟entry的前提條件都不具備窖贤。
現(xiàn)在對(duì)cleanSomeSlot方法做一下總結(jié),其方法執(zhí)行示意圖如下:
如圖所示,cleanSomeSlot方法主要有這樣幾點(diǎn):
- 從當(dāng)前位置i處(位于i處的entry一定不是臟entry)為起點(diǎn)在初始小范圍(log2(n)赃梧,n為哈希表已插入entry的個(gè)數(shù)size)開始向后搜索臟entry滤蝠,若在整個(gè)搜索過程沒有臟entry,方法結(jié)束退出
- 如果在搜索過程中遇到臟entryt通過expungeStaleEntry方法清理掉當(dāng)前臟entry授嘀,并且該方法會(huì)返回下一個(gè)哈希桶(table[i])為null的索引位置為i物咳。這時(shí)重新令搜索起點(diǎn)為索引位置i,n為哈希表的長度len粤攒,再次擴(kuò)大搜索范圍為log2(n')繼續(xù)搜索所森。
下面,以一個(gè)例子更清晰的來說一下夯接,假設(shè)當(dāng)前table數(shù)組的情況如下圖焕济。
如圖當(dāng)前n等于hash表的size即n=10,i=1,在第一趟搜索過程中通過nextIndex,i指向了索引為2的位置盔几,此時(shí)table[2]為null晴弃,說明第一趟未發(fā)現(xiàn)臟entry,則第一趟結(jié)束進(jìn)行第二趟的搜索。
第二趟所搜先通過nextIndex方法逊拍,索引由2的位置變成了i=3,當(dāng)前table[3]!=null但是該entry的key為null上鞠,說明找到了一個(gè)臟entry,先將n置為哈希表的長度len,然后繼續(xù)調(diào)用expungeStaleEntry方法芯丧,該方法會(huì)將當(dāng)前索引為3的臟entry給清除掉(令value為null芍阎,并且table[3]也為null),但是該方法可不想偷懶,它會(huì)繼續(xù)往后環(huán)形搜索缨恒,往后會(huì)發(fā)現(xiàn)索引為4,5的位置的entry同樣為臟entry谴咸,索引為6的位置的entry不是臟entry保持不變,直至i=7的時(shí)候此處table[7]位null骗露,該方法就以i=7返回岭佳。至此,第二趟搜索結(jié)束萧锉;
由于在第二趟搜索中發(fā)現(xiàn)臟entry珊随,n增大為數(shù)組的長度len,因此擴(kuò)大搜索范圍(增大循環(huán)次數(shù))繼續(xù)向后環(huán)形搜索柿隙;
直到在整個(gè)搜索范圍里都未發(fā)現(xiàn)臟entry叶洞,cleanSomeSlot方法執(zhí)行結(jié)束退出。
2.3 replaceStaleEntry
先來看replaceStaleEntry 方法禀崖,該方法源碼為:
/*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
//向前找到第一個(gè)臟entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
1. slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
//如果在向后環(huán)形查找過程中發(fā)現(xiàn)key相同的entry就覆蓋并且和臟entry進(jìn)行交換
2. e.value = value;
3. tab[i] = tab[staleSlot];
4. tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
//如果在查找過程中還未發(fā)現(xiàn)臟entry衩辟,那么就以當(dāng)前位置作為cleanSomeSlots
//的起點(diǎn)
if (slotToExpunge == staleSlot)
5. slotToExpunge = i;
//搜索臟entry并進(jìn)行清理
6. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
//如果向前未搜索到臟entry,則在查找過程遇到臟entry的話帆焕,后面就以此時(shí)這個(gè)位置
//作為起點(diǎn)執(zhí)行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
7. slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//如果在查找過程中沒有找到可以覆蓋的entry惭婿,則將新的entry插入在臟entry
8. tab[staleSlot].value = null;
9. tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
10. if (slotToExpunge != staleSlot)
//執(zhí)行cleanSomeSlots
11. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
該方法的邏輯請(qǐng)看注釋,下面我結(jié)合各種情況詳細(xì)說一下該方法的執(zhí)行過程叶雹。首先先看這一部分的代碼:
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
這部分代碼通過PreIndex方法實(shí)現(xiàn)往前環(huán)形搜索臟entry的功能财饥,初始時(shí)slotToExpunge和staleSlot相同,若在搜索過程中發(fā)現(xiàn)了臟entry折晦,則更新slotToExpunge為當(dāng)前索引i钥星。另外,說明replaceStaleEntry并不僅僅局限于處理當(dāng)前已知的臟entry满着,它認(rèn)為在出現(xiàn)臟entry的相鄰位置也有很大概率出現(xiàn)臟entry谦炒,所以為了一次處理到位,就需要向前環(huán)形搜索风喇,找到前面的臟entry宁改。那么根據(jù)在向前搜索中是否還有臟entry以及在for循環(huán)后向環(huán)形查找中是否找到可覆蓋的entry,我們分這四種情況來充分理解這個(gè)方法:
-
1.前向有臟entry
-
1.1后向環(huán)形查找找到可覆蓋的entry
該情形如下圖所示魂莫。
如圖,slotToExpunge初始狀態(tài)和staleSlot相同耙考,當(dāng)前向環(huán)形搜索遇到臟entry時(shí)谜喊,在第1行代碼中slotToExpunge會(huì)更新為當(dāng)前臟entry的索引i,直到遇到哈希桶(table[i])為null的時(shí)候倦始,前向搜索過程結(jié)束斗遏。在接下來的for循環(huán)中進(jìn)行后向環(huán)形查找,若查找到了可覆蓋的entry鞋邑,第2,3,4行代碼先覆蓋當(dāng)前位置的entry诵次,然后再與staleSlot位置上的臟entry進(jìn)行交換。交換之后臟entry就更換到了i處炫狱,最后使用cleanSomeSlots方法從slotToExpunge為起點(diǎn)開始進(jìn)行清理臟entry的過程
-
1.2后向環(huán)形查找未找到可覆蓋的entry
該情形如下圖所示藻懒。
如圖,slotToExpunge初始狀態(tài)和staleSlot相同视译,當(dāng)前向環(huán)形搜索遇到臟entry時(shí)嬉荆,在第1行代碼中slotToExpunge會(huì)更新為當(dāng)前臟entry的索引i,直到遇到哈希桶(table[i])為null的時(shí)候酷含,前向搜索過程結(jié)束鄙早。在接下來的for循環(huán)中進(jìn)行后向環(huán)形查找,若沒有查找到了可覆蓋的entry椅亚,哈希桶(table[i])為null的時(shí)候限番,后向環(huán)形查找過程結(jié)束。那么接下來在8,9行代碼中呀舔,將插入的新entry直接放在staleSlot處即可弥虐,最后使用cleanSomeSlots方法從slotToExpunge為起點(diǎn)開始進(jìn)行清理臟entry的過程
-
-
2.前向沒有臟entry
-
2.1后向環(huán)形查找找到可覆蓋的entry
該情形如下圖所示扩灯。
如圖霜瘪,slotToExpunge初始狀態(tài)和staleSlot相同珠插,當(dāng)前向環(huán)形搜索直到遇到哈希桶(table[i])為null的時(shí)候,前向搜索過程結(jié)束颖对,若在整個(gè)過程未遇到臟entry捻撑,slotToExpunge初始狀態(tài)依舊和staleSlot相同。在接下來的for循環(huán)中進(jìn)行后向環(huán)形查找缤底,若遇到了臟entry顾患,在第7行代碼中更新slotToExpunge為位置i。若查找到了可覆蓋的entry个唧,第2,3,4行代碼先覆蓋當(dāng)前位置的entry江解,然后再與staleSlot位置上的臟entry進(jìn)行交換,交換之后臟entry就更換到了i處徙歼。如果在整個(gè)查找過程中都還沒有遇到臟entry的話膘流,會(huì)通過第5行代碼,將slotToExpunge更新當(dāng)前i處鲁沥,最后使用cleanSomeSlots方法從slotToExpunge為起點(diǎn)開始進(jìn)行清理臟entry的過程呼股。
-
2.2后向環(huán)形查找未找到可覆蓋的entry
該情形如下圖所示。
如圖画恰,slotToExpunge初始狀態(tài)和staleSlot相同彭谁,當(dāng)前向環(huán)形搜索直到遇到哈希桶(table[i])為null的時(shí)候,前向搜索過程結(jié)束允扇,若在整個(gè)過程未遇到臟entry缠局,slotToExpunge初始狀態(tài)依舊和staleSlot相同。在接下來的for循環(huán)中進(jìn)行后向環(huán)形查找考润,若遇到了臟entry狭园,在第7行代碼中更新slotToExpunge為位置i。若沒有查找到了可覆蓋的entry糊治,哈希桶(table[i])為null的時(shí)候唱矛,后向環(huán)形查找過程結(jié)束。那么接下來在8,9行代碼中井辜,將插入的新entry直接放在staleSlot處即可绎谦。另外,如果發(fā)現(xiàn)slotToExpunge被重置粥脚,則第10行代碼if判斷為true,就使用cleanSomeSlots方法從slotToExpunge為起點(diǎn)開始進(jìn)行清理臟entry的過程窃肠。
-
下面用一個(gè)實(shí)例來有個(gè)直觀的感受,示例代碼就不給出了刷允,代碼debug時(shí)table狀態(tài)如下圖所示:
如圖所示冤留,當(dāng)前的staleSolt為i=4碧囊,首先先進(jìn)行前向搜索臟entry,當(dāng)i=3的時(shí)候遇到臟entry纤怒,slotToExpung更新為3呕臂,當(dāng)i=2的時(shí)候tabel[2]為null,因此前向搜索臟entry的過程結(jié)束肪跋。然后進(jìn)行后向環(huán)形查找,知道i=7的時(shí)候遇到table[7]為null土砂,結(jié)束后向查找過程州既,并且在該過程并沒有找到可以覆蓋的entry。最后只能在staleSlot(4)處插入新entry萝映,然后從slotToExpunge(3)為起點(diǎn)進(jìn)行cleanSomeSlots進(jìn)行臟entry的清理吴叶。是不是上面的1.2的情況。
這些核心方法序臂,通過源碼又給出示例圖蚌卤,應(yīng)該最終都能掌握了,也還挺有意思的奥秆。若覺得不錯(cuò)逊彭,對(duì)我的辛勞付出能給出鼓勵(lì)歡迎點(diǎn)贊,給小弟鼓勵(lì)构订,在此謝過 :)侮叮。
當(dāng)我們調(diào)用threadLocal的get方法時(shí),當(dāng)table[i]不是和所要找的key相同的話悼瘾,會(huì)繼續(xù)通過threadLocalMap的
getEntryAfterMiss方法向后環(huán)形去找囊榜,該方法為:
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)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
當(dāng)key==null的時(shí)候,即遇到臟entry也會(huì)調(diào)用expungeStleEntry對(duì)臟entry進(jìn)行清理亥宿。
當(dāng)我們調(diào)用threadLocal.remove方法時(shí)候卸勺,實(shí)際上會(huì)調(diào)用threadLocalMap的remove方法,該方法的源碼為:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
同樣的可以看出烫扼,當(dāng)遇到了key為null的臟entry的時(shí)候曙求,也會(huì)調(diào)用expungeStaleEntry清理掉臟entry。
從以上set,getEntry,remove方法看出映企,在threadLocal的生命周期里圆到,針對(duì)threadLocal存在的內(nèi)存泄漏的問題,都會(huì)通過expungeStaleEntry卑吭,cleanSomeSlots,replaceStaleEntry這三個(gè)方法清理掉key為null的臟entry芽淡。
2.4 為什么使用弱引用?
從文章開頭通過threadLocal,threadLocalMap,entry的引用關(guān)系看起來threadLocal存在內(nèi)存泄漏的問題似乎是因?yàn)閠hreadLocal是被弱引用修飾的豆赏。那為什么要使用弱引用呢挣菲?
如果使用強(qiáng)引用
假設(shè)threadLocal使用的是強(qiáng)引用富稻,在業(yè)務(wù)代碼中執(zhí)行threadLocalInstance==null
操作,以清理掉threadLocal實(shí)例的目的白胀,但是因?yàn)閠hreadLocalMap的Entry強(qiáng)引用threadLocal椭赋,因此在gc的時(shí)候進(jìn)行可達(dá)性分析,threadLocal依然可達(dá)或杠,對(duì)threadLocal并不會(huì)進(jìn)行垃圾回收哪怔,這樣就無法真正達(dá)到業(yè)務(wù)邏輯的目的,出現(xiàn)邏輯錯(cuò)誤
如果使用弱引用
假設(shè)Entry弱引用threadLocal向抢,盡管會(huì)出現(xiàn)內(nèi)存泄漏的問題认境,但是在threadLocal的生命周期里(set,getEntry,remove)里,都會(huì)針對(duì)key為null的臟entry進(jìn)行處理挟鸠。
從以上的分析可以看出叉信,使用弱引用的話在threadLocal生命周期里會(huì)盡可能的保證不出現(xiàn)內(nèi)存泄漏的問題,達(dá)到安全的狀態(tài)艘希。
2.5 Thread.exit()
當(dāng)線程退出時(shí)會(huì)執(zhí)行exit方法:
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
從源碼可以看出當(dāng)線程結(jié)束時(shí)硼身,會(huì)令threadLocals=null,也就意味著GC的時(shí)候就可以將threadLocalMap進(jìn)行垃圾回收覆享,換句話說threadLocalMap生命周期實(shí)際上thread的生命周期相同佳遂。
3. threadLocal最佳實(shí)踐
通過這篇文章對(duì)threadLocal的內(nèi)存泄漏做了很詳細(xì)的分析,我們可以完全理解threadLocal內(nèi)存泄漏的前因后果撒顿,那么實(shí)踐中我們應(yīng)該怎么做讶迁?
- 每次使用完ThreadLocal,都調(diào)用它的remove()方法核蘸,清除數(shù)據(jù)巍糯。
- 在使用線程池的情況下,沒有及時(shí)清理ThreadLocal客扎,不僅是內(nèi)存泄漏的問題祟峦,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。所以徙鱼,使用ThreadLocal就跟加鎖完要解鎖一樣宅楞,用完就清理。
參考資料
《java高并發(fā)程序設(shè)計(jì)》
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/