轉-ThreadLocal源碼解讀(大牛之作)

1. 背景

ThreadLocal源碼解讀,網(wǎng)上面早已經(jīng)泛濫了氓侧,大多比較淺偿凭,甚至有的連基本原理都說的很有問題,包括百度搜索出來的第一篇高訪問量博文妥凳,說ThreadLocal內部有個map懦底,鍵為線程對象坏快,太誤導人了往史。

ThreadLocal非常適合對Java多線程編程感興趣的程序員作為入門類閱讀花枫,原因兩方面:

  1. 加上注釋源碼也不過七八百行骄噪。
  2. 結構清晰尚困,代碼簡潔。

本文重點導讀ThreadLocal中的嵌套內部類ThreadLocalMap链蕊,對ThreadLocal本身API的介紹簡略帶過事甜。
讀ThreadLocal源碼,不讀ThreadLocalMap的實現(xiàn)滔韵,和沒看過沒多大差別逻谦。

2. 兩個問題

先回答兩個問題:

  1. 什么是ThreadLocal?
    ThreadLocal類顧名思義可以理解為線程本地變量陪蜻。也就是說如果定義了一個ThreadLocal邦马,每個線程往這個ThreadLocal中讀寫是線程隔離,互相之間不會影響的宴卖。它提供了一種將可變數(shù)據(jù)通過每個線程有自己的獨立副本從而實現(xiàn)線程封閉的機制滋将。

  2. 它大致的實現(xiàn)思路是怎樣的?
    Thread類有一個類型為ThreadLocal.ThreadLocalMap的實例變量threadLocals症昏,也就是說每個線程有一個自己的ThreadLocalMap随闽。ThreadLocalMap有自己的獨立實現(xiàn),可以簡單地將它的key視作ThreadLocal肝谭,value為代碼中放入的值(實際上key并不是ThreadLocal本身掘宪,而是它的一個弱引用)。每個線程在往某個ThreadLocal里塞值的時候攘烛,都會往自己的ThreadLocalMap里存魏滚,讀也是以某個ThreadLocal作為引用,在自己的map里找對應的key坟漱,從而實現(xiàn)了線程隔離鼠次。

3. ThreadLocal的API

ThreadLocal的API其實沒多少好介紹的,這些API介紹網(wǎng)上早已爛大街了。


image

4. ThreadLocalMap的源碼實現(xiàn)

ThreadLocalMap的源碼實現(xiàn)须眷,才是我們讀ThreadLocal源碼真正要領悟的竖瘾。看看大師Doug Lea和Joshua Bloch的鬼斧神工之作花颗。


image

ThreadLocalMap提供了一種為ThreadLocal定制的高效實現(xiàn)捕传,并且自帶一種基于弱引用的垃圾清理機制。
下面從基本結構開始一點點解讀扩劝。

4.1 存儲結構

既然是個map(注意不要與java.util.map混為一談庸论,這里指的是概念上的map),當然得要有自己的key和value棒呛,上面回答的問題2中也已經(jīng)提及聂示,我們可以將其簡單視作key為ThreadLocal,value為實際放入的值簇秒。之所以說是簡單視作鱼喉,因為實際上ThreadLocal中存放的是ThreadLocal的弱引用。我們來看看ThreadLocalMap里的節(jié)點是如何定義的趋观。

static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
    // 往ThreadLocal里實際塞入的值
    Object value;

    Entry(java.lang.ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry便是ThreadLocalMap里定義的節(jié)點扛禽,它繼承了WeakReference類,定義了一個類型為Object的value皱坛,用于存放塞到ThreadLocal里的值编曼。

4.2 為什么要弱引用

讀到這里,如果不問不答為什么是這樣的定義形式剩辟,為什么要用弱引用掐场,等于沒讀懂源碼。
因為如果這里使用普通的key-value形式來定義存儲結構贩猎,實質上就會造成節(jié)點的生命周期與線程強綁定熊户,只要線程沒有銷毀,那么節(jié)點在GC分析中一直處于可達狀態(tài)融欧,沒辦法被回收敏弃,而程序本身也無法判斷是否可以清理節(jié)點。弱引用是Java中四檔引用的第三檔噪馏,比軟引用更加弱一些麦到,如果一個對象沒有強引用鏈可達,那么一般活不過下一次GC欠肾。當某個ThreadLocal已經(jīng)沒有強引用可達瓶颠,則隨著它被垃圾回收,在ThreadLocalMap里對應的Entry的鍵值會失效刺桃,這為ThreadLocalMap本身的垃圾清理提供了便利粹淋。

4.3 類成員變量與相應方法

/**
 * 初始容量,必須為2的冪
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * Entry表,大小必須為2的冪
 */
private Entry[] table;

/**
 * 表里entry的個數(shù)
 */
private int size = 0;

/**
 * 重新分配表大小的閾值桃移,默認為0
 */
private int threshold; 

可以看到屋匕,ThreadLocalMap維護了一個Entry表或者說Entry數(shù)組,并且要求表的大小必須為2的冪借杰,同時記錄表里面entry的個數(shù)以及下一次需要擴容的閾值过吻。
顯然這里會產生一個問題,為什么必須是2的冪蔗衡?很好纤虽,但是目前還無法回答,帶著問題接著往下讀绞惦。

/**
 * 設置resize閾值以維持最壞2/3的裝載因子
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

/**
 * 環(huán)形意義的下一個索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 環(huán)形意義的上一個索引
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

ThreadLocal需要維持一個最壞2/3的負載因子逼纸,對于負載因子相信應該不會陌生,在HashMap中就有這個概念济蝉。
ThreadLocal有兩個方法用于得到上一個/下一個索引杰刽,注意這里實際上是環(huán)形意義下的上一個與下一個。

由于ThreadLocalMap使用線性探測法來解決散列沖突堆生,所以實際上Entry[]數(shù)組在程序邏輯上是作為一個環(huán)形存在的专缠。
關于開放尋址、線性探測等內容淑仆,可以參考網(wǎng)上資料或者TAOCP(《計算機程序設計藝術》)第三卷的6.4章節(jié)。

至此哥力,我們已經(jīng)可以大致勾勒出ThreadLocalMap的內部存儲結構蔗怠。下面是我繪制的示意圖。虛線表示弱引用吩跋,實線表示強引用寞射。


image

ThreadLocalMap維護了Entry環(huán)形數(shù)組,數(shù)組中元素Entry的邏輯上的key為某個ThreadLocal對象(實際上是指向該ThreadLocal對象的弱引用)锌钮,value為代碼中該線程往該ThreadLoacl變量實際塞入的值桥温。

4.4 構造函數(shù)

好的,接下來再來看看ThreadLocalMap的一個構造函數(shù)

/**
 * 構造一個包含firstKey和firstValue的map梁丘。
 * ThreadLocalMap是惰性構造的侵浸,所以只有當至少要往里面放一個元素的時候才會構建它。
 */
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table數(shù)組
    table = new Entry[INITIAL_CAPACITY];
    // 用firstKey的threadLocalHashCode與初始大小16取模得到哈希值
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 初始化該節(jié)點
    table[i] = new Entry(firstKey, firstValue);
    // 設置節(jié)點表大小為1
    size = 1;
    // 設定擴容閾值
    setThreshold(INITIAL_CAPACITY);
}

這個構造函數(shù)在set和get的時候都可能會被間接調用以初始化線程的ThreadLocalMap氛谜。

4.5 哈希函數(shù)

重點看一下上面構造函數(shù)中的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);這一行代碼掏觉。

ThreadLocal類中有一個被final修飾的類型為int的threadLocalHashCode,它在該ThreadLocal被構造的時候就會生成值漫,相當于一個ThreadLocal的ID澳腹,而它的值來源于

/*
 * 生成hash code間隙為這個魔數(shù),可以讓生成出來的值或者說ThreadLocal的ID較為均勻地分布在2的冪大小的數(shù)組中。
 */
private static final int HASH_INCREMENT = 0x61c88647;

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

可以看出酱塔,它是在上一個被構造出的ThreadLocal的ID/threadLocalHashCode的基礎上加上一個魔數(shù)0x61c88647的沥邻。這個魔數(shù)的選取與斐波那契散列有關,0x61c88647對應的十進制為1640531527羊娃。斐波那契散列的乘數(shù)可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769谋国,如果把這個值給轉為帶符號的int,則會得到-1640531527迁沫。換句話說
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的結果就是1640531527也就是0x61c88647芦瘾。通過理論與實踐,當我們用0x61c88647作為魔數(shù)累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模集畅,得到的結果分布很均勻近弟。
ThreadLocalMap使用的是線性探測法,均勻分布的好處在于很快就能探測到下一個臨近的可用slot挺智,從而保證效率祷愉。這就回答了上文拋出的為什么大小要為2的冪的問題。為了優(yōu)化效率赦颇。

對于& (INITIAL_CAPACITY - 1)二鳄,相信有過算法競賽經(jīng)驗或是閱讀源碼較多的程序員,一看就明白媒怯,對于2的冪作為模數(shù)取模订讼,可以用&(2n-1)來替代%2n,位運算比取模效率高很多扇苞。至于為什么欺殿,因為對2^n取模,只要不是低n位對結果的貢獻顯然都是0鳖敷,會影響結果的只能是低n位脖苏。

可以說在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)(其中key為一個ThreadLocal實例)這樣的代碼片段實質上就是在求一個ThreadLocal實例的哈希值定踱,只是在源碼實現(xiàn)中沒有將其抽為一個公用函數(shù)棍潘。

4.6 getEntry方法

這個方法會被ThreadLocal的get方法直接調用,用于獲取map中某個ThreadLocal存放的值崖媚。

private Entry getEntry(ThreadLocal<?> key) {
    // 根據(jù)key這個ThreadLocal的ID來獲取索引亦歉,也即哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 對應的entry存在且未失效且弱引用指向的ThreadLocal就是key,則命中返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 因為用的是線性探測至扰,所以往后找還是有可能能夠找到目標Entry的鳍徽。
        return getEntryAfterMiss(key, i, e);
    }
}

/*
 * 調用getEntry未直接命中的時候調用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 基于線性探測法不斷向后探測直到遇到空entry。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目標
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 該entry對應的ThreadLocal已經(jīng)被回收敢课,調用expungeStaleEntry來清理無效的entry
            expungeStaleEntry(i);
        } else {
            // 環(huán)形意義下往后面走
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

/**
 * 這個函數(shù)是ThreadLocal中核心清理函數(shù)阶祭,它做的事情很簡單:
 * 就是從staleSlot開始遍歷绷杜,將無效(弱引用指向對象被回收)清理,即對應entry中的value置為null濒募,將指向這個entry的table[i]置為null鞭盟,直到掃到空entry。
 * 另外瑰剃,在過程中還會對非空的entry作rehash齿诉。
 * 可以說這個函數(shù)的作用就是從staleSlot開始清理連續(xù)段中的slot(斷開強引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 因為entry對應的ThreadLocal已經(jīng)被回收晌姚,value設為null粤剧,顯式斷開強引用
    tab[staleSlot].value = null;
    // 顯式設置該entry為null,以便垃圾回收
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 清理對應ThreadLocal已經(jīng)被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 對于還沒有被回收的情況挥唠,需要做一次rehash抵恋。
             * 
             * 如果對應的ThreadLocal的ID對len取模出來的索引h不為當前位置i,
             * 則從h向后線性探測到第一個空的slot宝磨,把當前的entry給挪過去弧关。
             */
            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.
                 *
                 * 這段話提及了Knuth高德納的著作TAOCP(《計算機程序設計藝術》)的6.4章節(jié)(散列)
                 * 中的R算法唤锉。R算法描述了如何從使用線性探測的散列表中刪除一個元素世囊。
                 * R算法維護了一個上次刪除元素的index,當在非空連續(xù)段中掃到某個entry的哈希值取模后的索引
                 * 還沒有遍歷到時窿祥,會將該entry挪到index那個位置株憾,并更新當前位置為新的index,
                 * 繼續(xù)向后掃描直到遇到空的entry壁肋。
                 *
                 * ThreadLocalMap因為使用了弱引用号胚,所以其實每個slot的狀態(tài)有三種也即
                 * 有效(value未回收),無效(value已回收)浸遗,空(entry==null)。
                 * 正是因為ThreadLocalMap的entry有三種狀態(tài)箱亿,所以不能完全套高德納原書的R算法跛锌。
                 *
                 * 因為expungeStaleEntry函數(shù)在掃描過程中還會對無效slot清理將之轉為空slot,
                 * 如果直接套用R算法届惋,可能會出現(xiàn)具有相同哈希值的entry之間斷開(中間有空entry)髓帽。
                 */
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一個空的slot索引
    return i;
}

我們來回顧一下從ThreadLocal讀一個值可能遇到的情況:
根據(jù)入?yún)hreadLocal的threadLocalHashCode對表容量取模得到index

  • 如果index對應的slot就是要讀的threadLocal,則直接返回結果
  • 調用getEntryAfterMiss線性探測脑豹,過程中每碰到無效slot郑藏,調用expungeStaleEntry進行段清理;如果找到了key瘩欺,則返回結果entry
  • 沒有找到key必盖,返回null

4.7 set方法

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();
        // 找到對應的entry
        if (k == key) {
            e.value = value;
            return;
        }
        // 替換失效的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

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
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 找到了key,將其與無效的slot交換
        if (k == key) {
            // 更新對應slot的value值
            e.value = value;

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

            /*
             * 如果在整個掃描過程中(包括函數(shù)一開始的向前掃描與i之前的向后掃描)
             * 找到了之前的無效slot則以那個位置作為清理的起點歌粥,
             * 否則則以當前的i作為清理起點
             */
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 從slotToExpunge開始做一次連續(xù)段的清理塌忽,再做一次啟發(fā)式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果當前的slot已經(jīng)無效,并且向前掃描過程中沒有無效slot失驶,則更新slotToExpunge為當前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }

    // 如果key在table中不存在土居,則在原地放一個即可
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 在探測過程中如果發(fā)現(xiàn)任何無效slot,則做一次清理(連續(xù)段清理+啟發(fā)式清理)
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

/**
 * 啟發(fā)式地清理slot,
 * i對應entry是非無效(指向的ThreadLocal沒被回收嬉探,或者entry本身為空)
 * n是用于控制控制掃描次數(shù)的
 * 正常情況下如果log n次掃描沒有發(fā)現(xiàn)無效slot擦耀,函數(shù)就結束了
 * 但是如果發(fā)現(xiàn)了無效的slot,將n置為table的長度len涩堤,做一次連續(xù)段的清理
 * 再從下一個空的slot開始繼續(xù)掃描
 * 
 * 這個函數(shù)有兩處地方會被調用眷蜓,一處是插入的時候可能會被調用,另外個是在替換無效slot的時候可能會被調用定躏,
 * 區(qū)別是前者傳入的n為元素個數(shù)账磺,后者為table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情況下自己都不會是一個無效slot,所以從下一個開始判斷
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 擴大掃描控制因子
            n = len;
            removed = true;
            // 清理一個連續(xù)段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 做一次全量清理
    expungeStaleEntries();

    /*
     * 因為做了一次清理痊远,所以size很可能會變小垮抗。
     * ThreadLocalMap這里的實現(xiàn)是調低閾值來判斷是否需要擴容,
     * threshold默認為len*2/3碧聪,所以這里的threshold - threshold / 4相當于len/2
     */
    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) {
            /*
             * 個人覺得這里可以取返回值冒版,如果大于j的話取了用,這樣也是可行的逞姿。
             * 因為expungeStaleEntry執(zhí)行過程中是把連續(xù)段內所有無效slot都清理了一遍了辞嗡。
             */
            expungeStaleEntry(j);
        }
    }
}

/**
 * 擴容,因為需要保證table的容量len為2的冪滞造,所以擴容即擴大2倍
 */
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 {
                // 線性探測來存放Entry
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

我們來回顧一下ThreadLocal的set方法可能會有的情況

  • 探測過程中slot都不無效续室,并且順利找到key所在的slot,直接替換即可
  • 探測過程中發(fā)現(xiàn)有無效slot谒养,調用replaceStaleEntry挺狰,效果是最終一定會把key和value放在這個slot,并且會盡可能清理無效slot
    • 在replaceStaleEntry過程中买窟,如果找到了key丰泊,則做一個swap把它放到那個無效slot中,value置為新值
    • 在replaceStaleEntry過程中始绍,沒有找到key瞳购,直接在無效slot原地放entry
  • 探測沒有發(fā)現(xiàn)key,則在連續(xù)段末尾的后一個空位置放上entry亏推,這也是線性探測法的一部分学赛。放完后年堆,做一次啟發(fā)式清理,如果沒清理出去key罢屈,并且當前table大小已經(jīng)超過閾值了嘀韧,則做一次rehash,rehash函數(shù)會調用一次全量清理slot方法也即expungeStaleEntries缠捌,如果完了之后table大小超過了threshold - threshold / 4锄贷,則進行擴容2倍

4.8 remove方法

/**
 * 從map中刪除ThreadLocal
 */
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方法相對于getEntry和set方法比較簡單,直接在table中找key曼月,如果找到了谊却,把弱引用斷了做一次段清理。

5. ThreadLocal與內存泄漏

關于ThreadLocal是否會引起內存泄漏也是一個比較有爭議性的問題哑芹,其實就是要看對內存泄漏的準確定義是什么炎辨。
認為ThreadLocal會引起內存泄漏的說法是因為如果一個ThreadLocal對象被回收了,我們往里面放的value對于【當前線程->當前線程的threadLocals(ThreadLocal.ThreadLocalMap對象)->Entry數(shù)組->某個entry.value】這樣一條強引用鏈是可達的聪姿,因此value不會被回收碴萧。
認為ThreadLocal不會引起內存泄漏的說法是因為ThreadLocal.ThreadLocalMap源碼實現(xiàn)中自帶一套自我清理的機制。

之所以有關于內存泄露的討論是因為在有線程復用如線程池的場景中末购,一個線程的壽命很長破喻,大對象長期不被回收影響系統(tǒng)運行效率與安全。如果線程不會復用盟榴,用完即銷毀了也不會有ThreadLocal引發(fā)內存泄露的問題曹质。《Effective Java》一書中的第6條對這種內存泄露稱為unintentional object retention(無意識的對象保留)擎场。

當我們仔細讀過ThreadLocalMap的源碼羽德,我們可以推斷,如果在使用的ThreadLocal的過程中迅办,顯式地進行remove是個很好的編碼習慣宅静,這樣是不會引起內存泄漏。
那么如果沒有顯式地進行remove呢站欺?只能說如果對應線程之后調用ThreadLocal的get和set方法都有很高的概率會順便清理掉無效對象坏为,斷開value強引用,從而大對象被收集器回收镊绪。

但無論如何,我們應該考慮到何時調用ThreadLocal的remove方法洒忧。一個比較熟悉的場景就是對于一個請求一個線程的server如tomcat蝴韭,在代碼中對web api作一個切面,存放一些如用戶名等用戶信息熙侍,在連接點方法結束后榄鉴,再顯式調用remove履磨。

6. InheritableThreadLocal原理

對于InheritableThreadLocal,本文不作過多介紹庆尘,只是簡單略過剃诅。
ThreadLocal本身是線程隔離的,InheritableThreadLocal提供了一種父子線程之間的數(shù)據(jù)共享機制驶忌。

它的具體實現(xiàn)是在Thread類中除了threadLocals外還有一個inheritableThreadLocals對象矛辕。

image

在線程對象初始化的時候,會調用ThreadLocal的createInheritedMap從父線程的inheritableThreadLocals中把有效的entry都拷過來

image

可以看一下其中的具體實現(xiàn)

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                // 這里的childValue方法在InheritableThreadLocal中默認實現(xiàn)為返回本身值付魔,可以被重寫
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

還是比較簡單的聊品,做的事情就是以父線程的inheritableThreadLocalMap為數(shù)據(jù)源,過濾出有效的entry几苍,初始化到自己的inheritableThreadLocalMap中翻屈。其中childValue可以被重寫。

需要注意的地方是InheritableThreadLocal只是在子線程創(chuàng)建的時候會去拷一份父線程的inheritableThreadLocals妻坝。如果父線程是在子線程創(chuàng)建后再set某個InheritableThreadLocal對象的值伸眶,對子線程是不可見的。

7. 總結

本博文重點介紹了ThreadLocal中ThreadLocalMap的大致實現(xiàn)原理以及ThreadLocal內存泄露的問題以及簡略介紹InheritableThreadLocal刽宪。作為Josh Bloch和Doug Lea兩位大師之作厘贼,ThreadLocal本身實現(xiàn)的算法與技巧還是很優(yōu)雅的。在開發(fā)過程中纠屋,ThreadLocal用到恰到好處的話涂臣,可以消除一些代碼的重復。但也要注意過度使用ThreadLocal很容易加大類之間的耦合度與依賴關系(開發(fā)過程可能會不得不過度考慮某個ThreadLocal在調用時是否已有值售担,存放的是哪個類放的什么值)赁遗。

原地址:https://www.cnblogs.com/micrari/p/6790229.html

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市族铆,隨后出現(xiàn)的幾起案子岩四,更是在濱河造成了極大的恐慌,老刑警劉巖哥攘,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剖煌,死亡現(xiàn)場離奇詭異,居然都是意外死亡逝淹,警方通過查閱死者的電腦和手機耕姊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來栅葡,“玉大人茉兰,你說我怎么就攤上這事⌒来兀” “怎么了规脸?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵坯约,是天一觀的道長。 經(jīng)常有香客問我莫鸭,道長闹丐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任被因,我火速辦了婚禮卿拴,結果婚禮上,老公的妹妹穿的比我還像新娘氏身。我一直安慰自己巍棱,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布蛋欣。 她就那樣靜靜地躺著航徙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪陷虎。 梳的紋絲不亂的頭發(fā)上到踏,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音尚猿,去河邊找鬼窝稿。 笑死,一個胖子當著我的面吹牛凿掂,可吹牛的內容都是我干的伴榔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼庄萎,長吁一口氣:“原來是場噩夢啊……” “哼踪少!你這毒婦竟也來了?” 一聲冷哼從身側響起糠涛,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤短曾,失蹤者是張志新(化名)和其女友劉穎欧聘,沒想到半個月后强衡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖幔,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年砸脊,在試婚紗的時候發(fā)現(xiàn)自己被綠了具篇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡凌埂,死狀恐怖栽连,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤秒紧,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站挨下,受9級特大地震影響熔恢,放射性物質發(fā)生泄漏。R本人自食惡果不足惜臭笆,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一叙淌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愁铺,春花似錦鹰霍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至瓶竭,卻和暖如春督勺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斤贰。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工智哀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荧恍。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓瓷叫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親送巡。 傳聞我的和親對象是個殘疾皇子摹菠,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容