線程封閉之ThreadLocal源碼詳解

簡書江溢Jonny,轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!

本文內(nèi)容將基于JDK1.7的源碼進行討論,并且在文章的結(jié)尾,筆者將會給出一些經(jīng)驗之談,希望能給學(xué)習(xí)者帶來些幫助。


一、線程封閉

《Java并發(fā)編程實戰(zhàn)》一書中提到,“當訪問共享的可變數(shù)據(jù)時,通常需要使用同步。一種避免使用同步的方式就是不共享數(shù)據(jù)”。因此提出了“線程封閉”的概念,一種經(jīng)常使用線程封閉的應(yīng)用場景就是JDBC的Connection,通過線程封閉技術(shù),可以把鏈接對象封閉在某個線程內(nèi)部,從而避免出現(xiàn)多個線程共享同一個鏈接的情況。而線程封閉總共有三種類型的呈現(xiàn)形式:

1)Ad-hoc線程封閉。維護線程封閉性的職責(zé)由程序?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),導(dǎo)致這個類并非是一個線程安全的實現(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è)計?好問題,按照很多的人的想法里,應(yīng)該有兩種設(shè)計方式:

1)全局ConcurrentMap<Thread,T>結(jié)構(gòu)。該設(shè)計在對應(yīng)的ThreadLocal對象內(nèi)維持一個本地變量表,以當前線程(使用Thread.currentThread()方法)作為key,查找對應(yīng)的的本地變量(value值),那么這么設(shè)計存在什么問題呢?

第一,全局的ConcurrentMap<Thread, T>表,這類數(shù)據(jù)結(jié)構(gòu)雖然是一類分段式且線程安全的容器,但是這類容器仍然會有線程同步的的額外開銷;

第二,隨著線程的銷毀,原有的ConcurrentMap<Thread, T>沒有被回收,因此導(dǎo)致了內(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)的存在,仍然有對象被當前線程強引用,從而導(dǎo)致了該對象不能被GC,因此同樣也會導(dǎo)致內(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í)行時間可能很長,但是對應(yīng)的ThreadLocal對象生成時間未必有線程的執(zhí)行壽命那般長,在對應(yīng)ThreadLocal對象由該線程作為根節(jié)點出發(fā),邏輯上不可達時,就應(yīng)該可以被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é)果取到哈希表中對應(yīng)的元素??墒沁@個時候,又會遇到哈希算法的經(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方法中不能馬上找到對應(yīng)的Entry,將調(diào)用該方法
 *
 * @param  e table[i]對應(yīng)的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ù)直接查找對應(yīng)的位置失敗后,就會從當前的位置往后開始尋找,直到找到對應(yīng)的key值,另外,如果發(fā)現(xiàn)有key值已經(jīng)被GC了,那么相應(yīng)的,也應(yīng)該啟動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對應(yīng)的十進制為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)建一個新的表,表的大小是原來表大小的兩倍,然后再逐個復(fù)制原表內(nèi)容到新表中,如果發(fā)現(xiàn)有無效對象,則把Entry對象中對應(yīng)的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類對象找到對應(yīng)的值
 */
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中存放。
        // 既然當前哈希位原來對應(yīng)的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是復(fù)用的,因此每個Thread對應(yīng)的ThreadLocal空間也是被復(fù)用的,如果開發(fā)者不希望ThreadExecutorPool中的下一個Task能讀取到上一個Task在ThreadLocal中存入的信息,那就不應(yīng)該使用ThreadLocal。

舉個例子:

final ThreadLocal<String> threadLocal = 
       new ThreadLocal<String>();
// 線程池大小為1
ThreadPoolExecutor threadPoolExecutor = 
      new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, 
                    new LinkedBlockingDeque<Runnable>());
// 任務(wù)1
threadPoolExecutor.execute(new Runnable() {
    public void run() {
        threadLocal.set("first insert"); 
    }
});
// 任務(wù)2
threadPoolExecutor.execute(new Runnable() {
    public void run() {
        System.out.println(threadLocal.get());
    }
});

像這樣,第二個任務(wù)能讀取到第一個任務(wù)插入的數(shù)據(jù)。但是如果此時線程池中任務(wù)一拋出一個異常出來:

final ThreadLocal<String> threadLocal = 
                      new ThreadLocal<String>();
// 線程池大小為1
ThreadPoolExecutor threadPoolExecutor = 
  new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, 
             new LinkedBlockingDeque<Runnable>());
// 任務(wù)1
threadPoolExecutor.execute(new Runnable() {
    public void run() {
        threadLocal.set("first insert");
        // 拋一個異常
        throw new RuntimeException("throw a exception"); 
 
    }
});
// 任務(wù)2
threadPoolExecutor.execute(new Runnable() {
    public void run() {
        System.out.println(threadLocal.get());
    }
});

那么此時,第二個任務(wù)無法讀取到第一個任務(wù)插入的數(shù)據(jù)(因為第一個線程因為拋異常已經(jīng)死了,任務(wù)二用的是新線程執(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導(dǎo)致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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容