線程封閉之ThreadLocal源碼詳解

簡書江溢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è)計:


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)如圖所示欢策,虛線部分表示的是一個弱引用

Entry引用

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引用

可以看到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方法的流程:

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


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诱担,隨后出現(xiàn)的幾起案子鲫售,更是在濱河造成了極大的恐慌,老刑警劉巖该肴,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件情竹,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機秦效,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門雏蛮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阱州,你說我怎么就攤上這事挑秉。” “怎么了苔货?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵犀概,是天一觀的道長。 經(jīng)常有香客問我夜惭,道長姻灶,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任诈茧,我火速辦了婚禮产喉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敢会。我一直安慰自己曾沈,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布鸥昏。 她就那樣靜靜地躺著塞俱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吏垮。 梳的紋絲不亂的頭發(fā)上障涯,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音惫皱,去河邊找鬼像樊。 笑死尤莺,一個胖子當著我的面吹牛旅敷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颤霎,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼媳谁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了友酱?” 一聲冷哼從身側(cè)響起晴音,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缔杉,沒想到半個月后锤躁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡或详,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年系羞,在試婚紗的時候發(fā)現(xiàn)自己被綠了郭计。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡椒振,死狀恐怖昭伸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澎迎,我是刑警寧澤庐杨,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站夹供,受9級特大地震影響灵份,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜罩引,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一各吨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袁铐,春花似錦揭蜒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洒缀,卻和暖如春瑰谜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背树绩。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工萨脑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饺饭。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓渤早,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瘫俊。 傳聞我的和親對象是個殘疾皇子鹊杖,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353