深入理解 ThreadLocal (這些細(xì)節(jié)不應(yīng)忽略)

前言

對(duì)于 ThreadLocal 的使用刹前,并不難。但要深入理解 ThreadLocal 的實(shí)現(xiàn)方式裳仆,需要細(xì)細(xì)揣摩溉箕。寫(xiě)本文前脊另,我在網(wǎng)上看了很多關(guān)于 ThreadLocal 的分析,但卻感到遺憾约巷,因?yàn)楹芏辔恼麓嬖谥欢ㄕ`區(qū)偎痛,包括一些大牛關(guān)于 ThreadLocal 內(nèi)存溢出的講解。更遺憾的是独郎,我并沒(méi)有在網(wǎng)上看到關(guān)于 ThreadLocal 中很多巧妙的設(shè)計(jì)的講解踩麦,如 ThreadLocal 的 hashCode 算法,ThreadLocalMap 中的 開(kāi)方地址發(fā) 等氓癌,探究這些可以更深入理解 ThreadLocal 的設(shè)計(jì)思想谓谦。

簡(jiǎn)介

ThreadLocal 用一種存儲(chǔ)變量與線(xiàn)程綁定的方式,在每個(gè)線(xiàn)程中用自己的 ThreadLocalMap 安全隔離變量贪婉,為解決多線(xiàn)程程序的并發(fā)問(wèn)題提供了一種新的思路反粥,如為每個(gè)線(xiàn)程創(chuàng)建一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)連接。因?yàn)槭蔷€(xiàn)程綁定的疲迂,所以在很多場(chǎng)景也被用來(lái)實(shí)現(xiàn)線(xiàn)程參數(shù)傳遞才顿,如 Spring 的 RequestContextHolder。也因?yàn)槊總€(gè)線(xiàn)程擁有自己唯一的 ThreadLocalMap 尤蒿,所以 ThreadLocalMap 是天然線(xiàn)程安全的郑气。

ThreadLocal 存儲(chǔ)結(jié)構(gòu)

首先我們來(lái)聊一聊 ThreadLocal 在多線(xiàn)程運(yùn)行時(shí),各線(xiàn)程是如何存儲(chǔ)變量的腰池,假如我們現(xiàn)在定義兩個(gè) ThreadLocal 實(shí)例如下:

static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();

我們分別在三個(gè)線(xiàn)程中使用 ThreadLocal尾组,偽代碼如下:

// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);

// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);

// thread-3中
threadLocal_2 .set(client_3);

這三個(gè)線(xiàn)程都在運(yùn)行中,那此時(shí)各線(xiàn)程中的存數(shù)數(shù)據(jù)應(yīng)該如下圖所示:(在手機(jī)上畫(huà)的簡(jiǎn)圖示弓,字有點(diǎn)小讳侨,意思還是表達(dá)出來(lái)了)

ThreadLocal運(yùn)行時(shí)結(jié)構(gòu)

每個(gè)線(xiàn)程持有自己的 ThreadLocalMap,ThreadLocalMap 初始容量為16(即圖中的16個(gè)槽位)奏属,在調(diào)用ThreadLocal 的 set 方法時(shí)跨跨,將以 ThreadLocal 為 Key 存儲(chǔ)在 本線(xiàn)程的 ThreadLocalMap 里面,ThreadLocalMap 的 Value 為Object 類(lèi)型拍皮,實(shí)際類(lèi)型由 ThreadLocal 定義歹叮。圖沒(méi)有看懂的不要緊,一步一步往下看其運(yùn)行原理铆帽,再回頭看圖咆耿,會(huì)有更清晰的理解。

ThreadLocal public方法

1. ThreadLocal 之 set() 方法
public void set(T value) {
    Thread t = Thread.currentThread(); // 獲取當(dāng)前線(xiàn)程
    ThreadLocalMap map = getMap(t); // 拿到當(dāng)前線(xiàn)程的 ThreadLocalMap
    if (map != null) // 判斷 ThreadLocalMap 是否存在
        map.set(this, value); // 調(diào)用 ThreadLocalMap 的 set 方法
    else
        createMap(t, value); // 創(chuàng)建 ThreadLocalMap
}

第一次調(diào)用時(shí)需要 creatMap爹橱,創(chuàng)建方式比較簡(jiǎn)單萨螺,不詳解。這里重要的還是 ThreadLocalMap 的 set 方法愧驱。

2. ThreadLocal 之 get() 方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //調(diào)用  ThreadLocalMap 的 getEntry 方法
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); // 如果還沒(méi)有設(shè)置慰技,可以用子類(lèi)實(shí)現(xiàn) initialValue ,自定義初始值组砚。
}
3. ThreadLocal 之 remove() 方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this); // 調(diào)用 ThreadLocalMap 的 remove方法
}

這里羅列了 ThreadLocal 的幾個(gè)public方法吻商,其實(shí)所有工作最終都落到了 ThreadLocalMap 的頭上,ThreadLocal 僅僅是從當(dāng)前線(xiàn)程取到 ThreadLocalMap 而已糟红,具體執(zhí)行艾帐,請(qǐng)看下面對(duì) ThreadLocalMap 的分析。


ThreadLocalMap

ThreadLocalMap 簡(jiǎn)介:

ThreadLocalMap 是ThreadLocal 內(nèi)部的一個(gè)Map實(shí)現(xiàn)盆偿,然而它并沒(méi)有實(shí)現(xiàn)任何集合的接口規(guī)范柒爸,因?yàn)樗鼉H供內(nèi)部使用,數(shù)據(jù)結(jié)構(gòu)采用 數(shù)組 + 開(kāi)方地址法事扭,Entry 繼承 WeakReference捎稚,是基于 ThreadLocal 這種特殊場(chǎng)景實(shí)現(xiàn)的 Map,它的實(shí)現(xiàn)方式很值得研究求橄。

ThreadLocalMap 的 Entry 定義如下:

static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

Entry 繼承 WeakReference今野,以 ThreadLocal 為 key,類(lèi)似 WeakHashMap 罐农,對(duì)內(nèi)存敏感腥泥。雖然繼承 WeakReference,但只能實(shí)現(xiàn)對(duì) Reference 的 key 的回收啃匿,而對(duì) value 的回收需要手動(dòng)解決蛔外。value 何時(shí)被回收? 如果沒(méi)有理解 value 的回收時(shí)間溯乒,那可能留下內(nèi)存溢出的隱患夹厌。

PS:當(dāng) map.get() = null 的時(shí)候本文中將它稱(chēng)為 **過(guò)期**。

ThreadLocalMap 核心方法:

1. ThreadLocalMap 之 key 的 hashCode 計(jì)算

ThreadLocalMap 的 key 是 ThreadLocal裆悄,但它不會(huì)傳統(tǒng)的調(diào)用 ThreadLocal 的 hashCode 方法(繼承自O(shè)bject 的 hashCode)矛纹,而是調(diào)用 nextHashCode() ,具體運(yùn)算如下:

 private final int threadLocalHashCode = nextHashCode();

 private static AtomicInteger nextHashCode = new AtomicInteger();

 //1640531527 這是一個(gè)神奇的數(shù)字光稼,能夠讓hash槽位分布相當(dāng)均勻
 private static final int HASH_INCREMENT = 0x61c88647; 

 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
 }

在 ThreadLocalMap 中 的 hashCode 全部使用 threadLocalHashCode 字段或南。threadLocalHashCode 用 final 修飾孩等,不可變。threadLocalHashCode 的生成調(diào)用 nextHashCode()采够,所有 ThreadLocalMap 的 hashCode 使用靜態(tài)的 AtomicInteger 每次增加 1640531527 來(lái)產(chǎn)生肄方,對(duì)于魔數(shù) 1640531527 的工作原理,數(shù)學(xué)思想比較多蹬癌,這里寫(xiě)個(gè)demo看一下基于這種方式產(chǎn)生的hash分布多均勻:

public class ThreadLocalTest {

    public static void main(String[] args) {
        printAllSlot(8);
        printAllSlot(16);
        printAllSlot(32);
    }

    static void printAllSlot(int len) {
        System.out.println("********** len = " + len + " ************");
        for (int i = 1; i <= 64; i++) {
            ThreadLocal<String> t = new ThreadLocal<>();
            int slot = getSlot(t, len);
            System.out.print(slot + " ");
            if (i % len == 0)
                System.out.println(); // 分組換行
        }
    }

    /**
     * 獲取槽位
     * 
     * @param t ThreadLocal
     * @param len 模擬map的table的length
     * @throws Exception
     */
    static int getSlot(ThreadLocal<?> t, int len) {
        int hash = getHashCode(t);
        return hash & (len - 1);
    }

    /**
     * 反射獲取 threadLocalHashCode 字段权她,因?yàn)槠錇閜rivate的
     */
    static int getHashCode(ThreadLocal<?> t) {
        Field field;
        try {
            field = t.getClass().getDeclaredField("threadLocalHashCode");
            field.setAccessible(true);
            return (int) field.get(t);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }
}

上述代碼模擬了 ThreadLocal 做為 key 的hashCode產(chǎn)生,看看完美槽位分配:

********** len = 8 ************
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
2 1 0 7 6 5 4 3 
********** len = 16 ************
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 
********** len = 32 ************
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 
10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 

PS:注意 ThreadLocal 的 nextHashCode 是由 static 修飾的逝薪,他是一個(gè)共享變量隅要,所有的 ThreadLocal 共享一個(gè) AtomicInteger,在其基礎(chǔ)上 CAS 增加董济。

2. ThreadLocalMap 之 set() 方法
 private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計(jì)算槽位
    // hash沖突時(shí)步清,使用開(kāi)放地址法
    // 因?yàn)楠?dú)特和hash算法,導(dǎo)致hash沖突很少虏肾,一般不會(huì)走進(jìn)這個(gè)for循環(huán)
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同尼啡,則覆蓋value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,說(shuō)明 key 已經(jīng)被回收了询微,進(jìn)入替換方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過(guò)期的值崖瞭,并判斷是否需要擴(kuò)容
        rehash(); // 擴(kuò)容
}

這個(gè) set 方法涵蓋了很多關(guān)鍵點(diǎn):

  1. 開(kāi)放地址法:與我們常用的Map不同,java里大部分Map都是用鏈表發(fā)解決hash沖突的撑毛,而 ThreadLocalMap 采用的是開(kāi)發(fā)地址法书聚。
  2. hash算法:hash值算法的精妙之處上面已經(jīng)講了,均勻的 hash 算法使其可以很好的配合開(kāi)方地址法使用藻雌;
  3. 過(guò)期值清理:關(guān)于過(guò)期值的清理是網(wǎng)上討論比較多了雌续,因?yàn)橹灰嘘P(guān)于可能內(nèi)存溢出的話(huà)題,就會(huì)帶來(lái)很多噱頭和流量胯杭。

簡(jiǎn)單介紹一下開(kāi)放地址法和鏈表法:

開(kāi)放地址法:容易產(chǎn)生堆積問(wèn)題驯杜;不適于大規(guī)模的數(shù)據(jù)存儲(chǔ);散列函數(shù)的設(shè)計(jì)對(duì)沖突會(huì)有很大的影響做个;插入時(shí)可能會(huì)出現(xiàn)多次沖突的現(xiàn)象鸽心,刪除的元素是多個(gè)沖突元素中的一個(gè),需要對(duì)后面的元素作處理居暖,實(shí)現(xiàn)較復(fù)雜顽频;結(jié)點(diǎn)規(guī)模很大時(shí)會(huì)浪費(fèi)很多空間;

鏈地址法:處理沖突簡(jiǎn)單太闺,且無(wú)堆積現(xiàn)象糯景,平均查找長(zhǎng)度短;鏈表中的結(jié)點(diǎn)是動(dòng)態(tài)申請(qǐng)的,適合構(gòu)造表不能確定長(zhǎng)度的情況蟀淮;相對(duì)而言最住,拉鏈法的指針域可以忽略不計(jì),因此較開(kāi)放地址法更加節(jié)省空間怠惶。插入結(jié)點(diǎn)應(yīng)該在鏈?zhǔn)渍歉浚瑒h除結(jié)點(diǎn)比較方便,只需調(diào)整指針而不需要對(duì)其他沖突元素作調(diào)整甚疟。

ThreadLocalMap 為什么采用開(kāi)放地址法仗岖?
個(gè)人認(rèn)為由于 ThreadLocalMap 的 hashCode 的精妙設(shè)計(jì)逃延,使hash沖突很少览妖,并且 Entry 繼承 WeakReference, 很容易被回收揽祥,并開(kāi)方地址可以節(jié)省一些指針空間讽膏;然而恰恰由于開(kāi)方地址法的使用,使在處理hash沖突時(shí)的代碼很難懂拄丰,比如在replaceStaleEntry,cleanSomeSlots府树,expungeStaleEntry 等地方,然而真正調(diào)用這些方法的幾率卻比較辛习础奄侠;要把上述方法搞清楚,最好畫(huà)一畫(huà)開(kāi)方地址法發(fā)生hash沖突的狀態(tài)圖载矿,容易理解一點(diǎn)垄潮,本文不詳細(xì)探討。


下面對(duì) set 方法里面的幾個(gè)關(guān)鍵方法展開(kāi):

  1. replaceStaleEntry
    因?yàn)殚_(kāi)發(fā)地址發(fā)的使用闷盔,導(dǎo)致 replaceStaleEntry 這個(gè)方法有些復(fù)雜弯洗,它的清理工作會(huì)涉及到slot前后的非null的slot。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前尋找過(guò)期的slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到 key 或者 直到 遇到null 的slot 才終止循環(huán)
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到了key逢勾,那么需要將它與過(guò)期的 slot 交換來(lái)維護(hù)哈希表的順序牡整。
        // 然后可以將新過(guò)期的 slot 或其上面遇到的任何其他過(guò)期的 slot 
        // 給 expungeStaleEntry 以清除或 rehash 這個(gè) run 中的所有其他entries。

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果存在溺拱,則開(kāi)始清除前面過(guò)期的entry
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果我們沒(méi)有在向前掃描中找到過(guò)期的條目逃贝,
        // 那么在掃描 key 時(shí)看到的第一個(gè)過(guò)期 entry 是仍然存在于 run 中的條目。
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 如果沒(méi)有找到 key迫摔,那么在 slot 中創(chuàng)建新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果還有其他過(guò)期的entries存在 run 中秋泳,則清除他們
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中的 run 不好翻譯,理解為開(kāi)放地址中一個(gè)slot中前后不為null的連續(xù)entry

  1. cleanSomeSlots
    cleanSomeSlots 清除一些slot(一些攒菠?是不是有點(diǎn)模糊迫皱,到底是哪些?)
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);  // n = n / 2, 對(duì)數(shù)控制循環(huán) 
    return removed;
}

當(dāng)新元素被添加時(shí)卓起,或者另一個(gè)過(guò)期元素已被刪除時(shí)和敬,會(huì)調(diào)用cleanSomeSlots。該方法會(huì)試探性地掃描一些 entry 尋找過(guò)期的條目戏阅。它執(zhí)行 對(duì)數(shù) 數(shù)量的掃描昼弟,是一種 基于不掃描(快速但保留垃圾)所有元素掃描之間的平衡。

上面說(shuō)到的對(duì)數(shù)數(shù)量是多少奕筐?循環(huán)次數(shù) = log2(N) (log以2為底N的對(duì)數(shù))舱痘,此處N是map的size,如:

log2(4) = 2
log2(5) = 2
log2(18) = 4

因此离赫,此方法并沒(méi)有真正的清除芭逝,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面

  1. expungeStaleEntry(int staleSlot)

這里是真正的清除渊胸,并且不要被方法名迷惑旬盯,不僅僅會(huì)清除當(dāng)前過(guò)期的slot,還回往后查找直到遇到null的slot為止翎猛。開(kāi)發(fā)地址法的清除也較難理解胖翰,清除當(dāng)前slot后還有往后進(jìn)行rehash。

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清除當(dāng)前過(guò)期的slot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash 直到 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) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
3. ThreadLocalMap 之 getEntry() 方法

getEntry() 主要是在 ThreadLocal 的 get() 方法里被調(diào)用

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // 無(wú)hash沖突情況
        return e;
    else
        return getEntryAfterMiss(key, i, e); // 有hash沖突情況
}

該方法比較簡(jiǎn)潔切厘,首先運(yùn)算槽位 i 萨咳,然后判斷 table[i] 是否是目標(biāo)entry,不是則進(jìn)入 getEntryAfterMiss(key, i, e)疫稿;

下面展開(kāi) getEntryAfterMiss 方法:

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); // 此方法上面已經(jīng)講過(guò)了
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

這個(gè)方法是在遇到 hash 沖突時(shí)往后繼續(xù)查找培他,并且會(huì)清除查找路上遇到的過(guò)期slot。

4. ThreadLocalMap 之 rehash() 方法
private void rehash() {
    expungeStaleEntries();

   // 在上面的清除過(guò)程中而克,size會(huì)減小靶壮,在此處重新計(jì)算是否需要擴(kuò)容
   // 并沒(méi)有直接使用threshold,而是用較低的threshold (約 threshold 的 3/4)提前觸發(fā)resize
    if (size >= threshold - threshold / 4)
        resize();
}

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

rehash() 里首先調(diào)用 expungeStaleEntries()员萍,然后循環(huán)調(diào)用 expungeStaleEntry(j) ,此方法會(huì)清除所有過(guò)期的slot腾降。

繼續(xù)看 resize():

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; // Help the GC
            } 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;
}

resize() 方法里也會(huì)過(guò)濾掉一些 過(guò)期的 entry。

PS :ThreadLocalMap 沒(méi)有 影響因子 的字段碎绎,是采用直接設(shè)置 threshold 的方式螃壤,threshold = len * 2 / 3,相當(dāng)于不可修改的影響因子為 2/3筋帖,比 HashMap 的默認(rèn) 0.75 要低奸晴。這也是減少hash沖突的方式。

5. ThreadLocalMap 之 remove(key) 方法
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;
        }
    }
}

remove 方法是刪除特定的 ThreadLocal日麸,建議在 ThreadLocal 使用完后執(zhí)行此方法寄啼。

總結(jié)

一逮光、ThreadLocalMap 的 value 清理觸發(fā)時(shí)間:

  1. set(ThreadLocal<?> key, Object value)
    若無(wú)hash沖突,則先向后檢測(cè)log2(N)個(gè)位置墩划,發(fā)現(xiàn)過(guò)期 slot 則清除涕刚,如果沒(méi)有任何 slot 被清除,則判斷 size >= threshold乙帮,超過(guò)閥值會(huì)進(jìn)行 rehash()杜漠,rehash()會(huì)清除所有過(guò)期的value;
  2. getEntry(ThreadLocal<?> key) (ThreadLocal 的 get() 方法調(diào)用)
    如果沒(méi)有直接在hash計(jì)算的 slot 中找到entry察净, 則需要向后繼續(xù)查找(直到null為止)驾茴,查找期間發(fā)現(xiàn)的過(guò)期 slot 會(huì)被清除;
  3. remove(ThreadLocal<?> key)
    remove 不僅會(huì)清除需要清除的 key氢卡,還是清除hash沖突的位置的已過(guò)期的 key锈至;

清晰了以上過(guò)程,相信對(duì)于 ThreadLocal 的 內(nèi)存溢出問(wèn)題會(huì)有自己的看法异吻。在實(shí)際開(kāi)發(fā)中裹赴,不應(yīng)亂用 ThreadLocal 喜庞,如果使用 ThreadLocal 發(fā)生了內(nèi)存溢出诀浪,那應(yīng)該考慮是否使用合理。

PS:這里的清除并不代表被回收延都,只是把 value 置為 null雷猪,value 的具體回收時(shí)間由 垃圾收集器 決定。

二晰房、ThreadLocalMap 的 hash 算法和 開(kāi)方地址法

由于 ThreadLocal 在每個(gè) Thread 里面的唯一性和特殊性求摇,為其定制了特殊的 hashCode 生成方式,能夠很好的散列在 table 中殊者,有效的減少hash沖突与境。
基于較少的hash沖突,于是采用了開(kāi)放地址法猖吴,開(kāi)放地址法在沒(méi)有hash沖突的時(shí)候很好理解摔刁,在發(fā)生沖突時(shí)的代碼就有些繞。因此理解 ThreadLocalMap 的新增海蔽、刪除共屈、查找、清除等操作党窜,需要對(duì)開(kāi)方地址法的hash沖突處理有較清晰的思路拗引,最好在手邊畫(huà)一畫(huà)開(kāi)放地址法的hash沖突情況,目前沒(méi)有在網(wǎng)上找的很好的講解幌衣,爭(zhēng)取在后續(xù)文章補(bǔ)充矾削。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子哼凯,更是在濱河造成了極大的恐慌垦细,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挡逼,死亡現(xiàn)場(chǎng)離奇詭異括改,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)家坎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)嘱能,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人虱疏,你說(shuō)我怎么就攤上這事惹骂。” “怎么了做瞪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵对粪,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我装蓬,道長(zhǎng)著拭,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任牍帚,我火速辦了婚禮儡遮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暗赶。我一直安慰自己鄙币,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布蹂随。 她就那樣靜靜地躺著十嘿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪岳锁。 梳的紋絲不亂的頭發(fā)上绩衷,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音浸锨,去河邊找鬼唇聘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛柱搜,可吹牛的內(nèi)容都是我干的迟郎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼聪蘸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼宪肖!你這毒婦竟也來(lái)了表制?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤控乾,失蹤者是張志新(化名)和其女友劉穎么介,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蜕衡,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壤短,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了慨仿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片久脯。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖镰吆,靈堂內(nèi)的尸體忽然破棺而出帘撰,到底是詐尸還是另有隱情,我是刑警寧澤万皿,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布摧找,位于F島的核電站,受9級(jí)特大地震影響牢硅,放射性物質(zhì)發(fā)生泄漏蹬耘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一唤衫、第九天 我趴在偏房一處隱蔽的房頂上張望婆赠。 院中可真熱鬧绵脯,春花似錦佳励、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至悴侵,卻和暖如春瞧剖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背可免。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工抓于, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浇借。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓捉撮,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親妇垢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子巾遭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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