一篇文章磅废,從源碼深入詳解ThreadLocal內(nèi)存泄漏問題

原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑

詳情請(qǐng)戳www.codercc.com

image

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)系如下圖所示:

threadLocal引用示意圖

上圖中晰筛,實(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做了這樣的處理:

  1. 如果當(dāng)前table[i]奋岁!=null的話說明hash沖突就需要向后環(huán)形查找,若在查找過程中遇到臟entry就通過replaceStaleEntry進(jìn)行處理荸百;
  2. 如果當(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>

  1. i表示:插入entry的位置i,很顯然在上述情況2(table[i]==null)中管搪,entry剛插入后該位置i很顯然不是臟entry;

  2. 參數(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ò)大媒至,示意圖如下

cleanSomeSlots示意圖.png

按照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步)免猾,主要做了這么幾件事情:

  1. 清理當(dāng)前臟entry是辕,即將其value引用置為null,并且將table[staleSlot]也置為null猎提。value置為null后該value域變?yōu)椴豢蛇_(dá)获三,在下一次gc的時(shí)候就會(huì)被回收掉,同時(shí)table[staleSlot]為null后以便于存放新的entry;
  2. 從當(dāng)前staleSlot位置向后環(huán)形(nextIndex)繼續(xù)搜索锨苏,直到遇到哈希桶(tab[i])為null的時(shí)候退出疙教;
  3. 若在搜索過程再次遇到臟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í)行示意圖如下:

cleanSomeSlots示意圖.png

如圖所示,cleanSomeSlot方法主要有這樣幾點(diǎn):

  1. 從當(dāng)前位置i處(位于i處的entry一定不是臟entry)為起點(diǎn)在初始小范圍(log2(n)赃梧,n為哈希表已插入entry的個(gè)數(shù)size)開始向后搜索臟entry滤蝠,若在整個(gè)搜索過程沒有臟entry,方法結(jié)束退出
  2. 如果在搜索過程中遇到臟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ù)組的情況如下圖焕济。

cleanSomeSlots執(zhí)行情景圖.png
  1. 如圖當(dāng)前n等于hash表的size即n=10,i=1,在第一趟搜索過程中通過nextIndex,i指向了索引為2的位置盔几,此時(shí)table[2]為null晴弃,說明第一趟未發(fā)現(xiàn)臟entry,則第一趟結(jié)束進(jìn)行第二趟的搜索。

  2. 第二趟所搜先通過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é)束萧锉;

  3. 由于在第二趟搜索中發(fā)現(xiàn)臟entry珊随,n增大為數(shù)組的長度len,因此擴(kuò)大搜索范圍(增大循環(huán)次數(shù))繼續(xù)向后環(huán)形搜索柿隙;

  4. 直到在整個(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

      該情形如下圖所示魂莫。


      向前環(huán)形搜索到臟entry还蹲,向后環(huán)形查找到可覆蓋的entry的情況.png

      如圖,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
      該情形如下圖所示藻懒。


      前向環(huán)形搜索到臟entry,向后環(huán)形未搜索可覆蓋entry.png

      如圖,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
      該情形如下圖所示扩灯。


      前向未搜索到臟entry,后向環(huán)形搜索到可覆蓋的entry.png

      如圖霜瘪,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
      該情形如下圖所示。


      前向環(huán)形未搜索到臟entry,后向環(huán)形查找未查找到可覆蓋的entry.png

      如圖画恰,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)如下圖所示:

1.2情況示意圖.png

如圖所示冤留,當(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)該怎么做讶迁?

  1. 每次使用完ThreadLocal,都調(diào)用它的remove()方法核蘸,清除數(shù)據(jù)巍糯。
  2. 在使用線程池的情況下,沒有及時(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末袱吆,一起剝皮案震驚了整個(gè)濱河市厌衙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绞绒,老刑警劉巖婶希,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蓬衡,居然都是意外死亡喻杈,警方通過查閱死者的電腦和手機(jī)彤枢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筒饰,“玉大人缴啡,你說我怎么就攤上這事〈擅牵” “怎么了业栅?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谬晕。 經(jīng)常有香客問我碘裕,道長,這世上最難降的妖魔是什么固蚤? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮歹茶,結(jié)果婚禮上夕玩,老公的妹妹穿的比我還像新娘。我一直安慰自己惊豺,他們只是感情好燎孟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著尸昧,像睡著了一般揩页。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烹俗,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天爆侣,我揣著相機(jī)與錄音,去河邊找鬼幢妄。 笑死兔仰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蕉鸳。 我是一名探鬼主播乎赴,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼潮尝!你這毒婦竟也來了榕吼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤勉失,失蹤者是張志新(化名)和其女友劉穎羹蚣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乱凿,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡度宦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年踢匣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戈抄。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡离唬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出划鸽,到底是詐尸還是另有隱情输莺,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布裸诽,位于F島的核電站嫂用,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丈冬。R本人自食惡果不足惜嘱函,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望埂蕊。 院中可真熱鬧往弓,春花似錦、人聲如沸蓄氧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喉童。三九已至撇寞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間堂氯,已是汗流浹背蔑担。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咽白,地道東北人钟沛。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像局扶,于是被迫代替她去往敵國和親恨统。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑 詳情請(qǐng)戳www.codercc.com 1. ThreadLoc...
    你聽___閱讀 6,725評(píng)論 8 19
  • Android Handler機(jī)制系列文章整體內(nèi)容如下: Android Handler機(jī)制1之ThreadAnd...
    隔壁老李頭閱讀 7,609評(píng)論 4 30
  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西三妈,可以用來干嘛畜埋。但面試時(shí)卻又經(jīng)常問到,所以這次我和大家一起學(xué)...
    liangzzz閱讀 12,426評(píng)論 14 228
  • 你多久沒有聞到過新磨的面粉在平底鍋里發(fā)出滋滋的畴蒲,淀粉由生到熟的特有的糊焦味兒了悠鞍?多久沒有聞到空氣在干燥而晴朗的天空...
    野草凝香閱讀 433評(píng)論 0 2
  • “那你一定還記得,我們?cè)?jīng)在大河游民的船上買過一些仿制品項(xiàng)鏈,幾乎把他們的貨袋子都掏空了咖祭,它們后來可都進(jìn)了皮爾特沃...
    冥月十宮閱讀 274評(píng)論 0 0