ThreadLocal和ThreadLocalMap源碼分析

背景分析

​ 相信不少程序猿在日常實現功能的過程中,都會遇到想要某些靜態變量,無論是單線程亦或者是多線程在使用,都不會產生相互之間的影響,也就是這個靜態變量在線程之間是讀寫隔離的。java

​ 有一個咱們常用的工具類,它的併發問題就是用ThreadLocal來解決的,我相信大多數人都看過,那就是SimpleDateFormat日期格式化的工具類的多線程問題,你們去網上搜的話,應該會有一堆人都說使用ThreadLocal。數組

定義

​ 那究竟何謂ThreadLocal呢?經過咱們的Chinese English,咱們也能夠翻譯出來,那就是線程本地的意思,並且咱們是用來存放咱們須要可以線程隔離的變量的,那就是線程本地變量。也就是說,當咱們把變量保存在ThreadLocal當中時,就可以實現這個變量的線程隔離了。安全

例子

​ 咱們先來看兩個例子,這裏也恰好涉及到兩個概念,分別是值傳遞引用傳遞數據結構

  • 值傳遞
public class ThreadLocalTest {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0;
        }
    };

    // 值傳遞
    @Test
    public void testValue(){
        for (int i = 0; i < 5; i++){
            new Thread(() -> {
                Integer temp = threadLocal.get();
                threadLocal.set(temp + 5);
                System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal.get());
            }, "thread-" + i).start();
        }
    }
}
複製代碼

以上程序的輸出結果是:多線程

current thread is thread-1 num is 5
current thread is thread-3 num is 5
current thread is thread-0 num is 5
current thread is thread-4 num is 5
current thread is thread-2 num is 5
複製代碼

​ 咱們能夠看到,每個線程打印出來的都是5,哪怕我是先經過ThreadLocal.get()方法獲取變量,而後再set進去,依然不會進行重複疊加。併發

​ 這就是線程隔離。函數

​ 可是對於引用傳遞來講,咱們又須要多注意一下了,直接上例子看看。工具

  • 引用傳遞
public class ThreadLocalTest {

    static NumIndex numIndex = new NumIndex();
    private static ThreadLocal<NumIndex> threadLocal1 = new ThreadLocal<NumIndex>(){
        protected NumIndex initialValue(){
            return numIndex;
        }
    };

    static class NumIndex{
        int num = 0;
        public void increment(){
            num++;
        }
    }

    // 引用傳遞
    @Test
    public void testReference(){
        for (int i = 0; i < 5; i++){
            new Thread(() -> {
                NumIndex index = threadLocal1.get();
                index.increment();
                threadLocal1.set(index);
                System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal1.get().num);
            }, "thread-" + i).start();
        }
    }
}
複製代碼

​ 咱們看看運行的結果源碼分析

current thread is thread-0 num is 2
current thread is thread-2 num is 3
current thread is thread-1 num is 2
current thread is thread-4 num is 5
current thread is thread-3 num is 4
複製代碼

​ 咱們看到值不但沒有被隔離,並且還出現了線程安全的問題。學習

因此咱們必定要注意值傳遞和引用傳遞的區別,在這裏也不講這兩個概念了。

源碼分析

​ 想要更加深刻地瞭解ThreadLocal這個東西的做用,最後仍是得回到擼源碼,看看==Josh Bloch and Doug Lea==這兩位大神到底是怎麼實現的?整個類加起來也不過七八百行而已。

​ 在這裏,我分開兩部分來講,分別是ThreadLocalThreadLocalMap這兩個的源碼分析。

ThreadLocalMap源碼分析

​ 思而再三,最後仍是決定先講ThreadLocalMap的源碼解析,爲何呢?

ThreadLocalMapThreadLocal裏面的一個靜態內部類,可是確實一個很關鍵的東西,咱們既然是在看源碼而且想要弄懂這個東西,那咱們就必定要有一種思惟,那就是若是是咱們要實現這麼個功能,咱們要怎麼作?以及看到別人的代碼,要學會思考別人爲何要這麼作?

​ 我但願經過個人文章,不求可以帶給你什麼牛逼的技術,可是至少能讓你明白,咱們須要學習的是這些大牛的嚴謹的思惟邏輯。

​ 言歸正傳,ThreadLocalMap到底是什麼?咱們要這麼想,既然是線程本地變量,並且咱們能夠經過get和set方法可以獲取和賦值。

​ 一、那咱們賦值的內容,究竟保存在什麼結構當中?

​ 二、它到底是怎麼作到線程隔離的?

​ 三、當我get和set的時候,它到底是怎麼作到線程-value的對應關係進行保存的?

​ 經過以上三個問題,再結合ThreadLocalMap這個名字,我想你們也知道這個是什麼了。

​ 沒錯,它就是ThreadLocal很是核心的內容,是維護咱們線程與變量之間關係的一個類,看到是Map結尾,那咱們也可以知道它實際上就是一個鍵值對。至於KEY是什麼,咱們會在源碼分析當中看出來。

Entry內部類

​ 如下源碼都是抽取講解部分的內容來展現

static class ThreadLocalMap {

    /** * 自定義一個Entry類,並繼承自弱引用 * 用來保存ThreadLocal和Value之間的對應關係 * * 之因此用弱引用,是爲了解決線程與ThreadLocal之間的強綁定關係 * 會致使若是線程沒有被回收,則GC便一直沒法回收這部份內容 */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    /** * The initial capacity -- MUST be a power of two. * Entry數組的初始化大小 */
    private static final int INITIAL_CAPACITY = 16;

    /** * The table, resized as necessary. * table.length MUST always be a power of two. * <ThreadLocal, 保存的泛型值>數組 * 長度必須是2的N次冪 * 這個能夠參考爲何HashMap裏維護的數組也必須是2的N次冪 * 主要是爲了減小碰撞,可以讓保存的元素儘可能的分散 * 關鍵代碼仍是hashcode & table.length - 1 */
    private Entry[] table;

    /** * The number of entries in the table. * table裏的元素個數 */
    private int size = 0;

    /** * The next size value at which to resize. * 擴容的閾值 */
    private int threshold; // Default to 0

    /** * Set the resize threshold to maintain at worst a 2/3 load factor. * 根據長度計算擴容的閾值 */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    /** * 經過如下兩個獲取next和prev的代碼能夠看出,entry數組其實是一個環形結構 */
    /** * Increment i modulo len. * 獲取下一個索引,超出長度則返回0 */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /** * Decrement i modulo len. * 返回上一個索引,若是-1爲負數,返回長度-1的索引 */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

    /** * 構造參數建立一個ThreadLocalMap代碼 * ThreadLocal爲key,咱們的泛型爲value */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 初始化table的大小爲16
        table = new Entry[INITIAL_CAPACITY];

        // 經過hashcode & (長度-1)的位運算,肯定鍵值對的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

        // 建立一個新節點保存在table當中
        table[i] = new Entry(firstKey, firstValue);

        // 設置table內元素爲1
        size = 1;

        // 設置擴容閾值
        setThreshold(INITIAL_CAPACITY);
    }

    /** * ThreadLocal自己是線程隔離的,按道理是不會出現數據共享和傳遞的行爲的 * 這是InheritableThreadLocal提供了了一種父子間數據共享的機制 * @param parentMap the map associated with parent thread. */
    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) {
                    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++;
                }
            }
        }
    }
}
複製代碼

​ 一些簡單的東西直接看我上面的註釋就能夠了。

​ 咱們能夠看到,在ThreadLocalMap這個內部類當中,又定義了一個Entry內部類,而且繼承自弱引用,泛型是ThreadLocal,其中有一個構造方法,經過這個咱們就大體能夠猜出,ThreadLocalMap當中的key實際上就是當前ThreadLocal對象。

​ 至於爲何要用弱引用呢?我想我源碼上面的註釋其實也寫得很明白了,這ThreadLocal實際上就是個線程本地變量隔離做用的工具類而已,當線程走完了,確定但願能回收這部分產生的資源,因此就用了弱引用。

​ 我相信有人會有疑問,若是在我要用的時候,被回收了怎麼辦?下面的代碼會一步步地讓你明白,你考慮到的問題,這些大牛都已經想到而且解決了。接着往下學吧!

getEntry和getEntryAfterMiss方法

​ 經過方法名咱們就能看得出是從ThreadLocal對應的ThreadLocalMap當中獲取Entry節點,在這咱們就要思考了。

​ 1)咱們要經過什麼獲取對應的Entry

​ 2)咱們經過上面知道使用了弱引用,若是被GC回收了沒有獲取到怎麼辦?

​ 3)不在經過計算獲得的下標上,又要怎麼辦?

​ 4)若是ThreadLocal對應的ThreadLocalMap不存在要怎麼辦?

​ 以上這4個問題是我本身在看源碼的時候可以想到的東西,有些問題的答案光看THreadLocalMap的源碼是看不出因此然的,須要結合以後的ThreadLocal源碼分析

​ 在這咱們來看看大牛的源碼是怎麼解決以上問題的吧。

/** * 獲取ThreadLocal的索引位置,經過下標索引獲取內容 */
private Entry getEntry(ThreadLocal<?> key) {
    // 經過hashcode肯定下標
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];

    // 若是找到則直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 找不到的話接着從i位置開始向後遍歷,基於線性探測法,是有可能在i以後的位置找到的
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 循環向後遍歷
    while (e != null) {

        // 獲取節點對應的k
        ThreadLocal<?> k = e.get();

        // 相等則返回
        if (k == key)
            return e;

        // 若是爲null,觸發一次連續段清理
        if (k == null)
            expungeStaleEntry(i);

        // 獲取下一個下標接着進行判斷
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
複製代碼

​ 一看這兩個方法名,咱們就知道這兩個方法就是獲取Entry節點的方法。

​ 咱們首先看getEntry(ThreadLocal<?> key)getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)這個方法就看出來了,直接根據ThreadLocal對象來獲取,因此咱們能夠再次證實,key就是ThreadLocal對象,咱們來看看它的流程

​ 一、首先根據key的hashcode & table.length - 1來肯定在table當中的下標

​ 二、若是獲取到直接返回,沒獲取到的話,就接着日後遍歷看是否能獲取到(由於用的是線性探測法,日後遍歷有可能獲取到結果)

​ 三、進入了getEntryAfterMiss方法進行線性探測,若是獲取到則直接返回;獲取的key爲null,則觸發一次連續段清理(實際上在不少方法當中都會觸發該方法,常常會進行連續段清理,這是ThreadLocal核心的清理方法)。

expungeStaleEntry方法

​ 這能夠說是ThreadLocal很是核心的一個清理方法,爲何會須要清理呢?或許不少人想不明白,咱們用List或者是Map也好,都沒有說要清理裏面的內容。

​ 可是這裏是對於線程來講的隔離的本地變量,而且使用的是弱引用,那便有可能在GC的時候就被回收了。

​ 1)若是有不少Entry節點已經被回收了,可是在table數組中還留着位置,這時候不清理就會浪費資源

​ 2)在清理節點的同時,能夠將後續非空的Entry節點從新計算下標進行排放,這樣子在get的時候就能快速定位資源,加快效率。

​ 咱們來看看別人源碼是怎麼作的吧!

/** * 這個函數能夠看作是ThreadLocal裏的核心清理函數,它主要作的事情就是 * 一、從staleSlot開始,向後遍歷將ThreadLocal對象被回收所在Entry節點的value和Entry節點自己設置null,方便GC,而且size自減1 * 二、而且會對非null的Entry節點進行rehash,只要不是在當前位置,就會將Entry挪到下一個爲null的位置上 * 因此其實是對從staleSlot開始作一個連續段的清理和rehash操做 */
private int expungeStaleEntry(int staleSlot) {
    // 新的引用指向table
    Entry[] tab = table;

    // 獲取長度
    int len = tab.length;

    // expunge entry at staleSlot
    // 先將傳過來的下標置null
    tab[staleSlot].value = null;
    tab[staleSlot] = null;

    // table的size-1
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    // 遍歷刪除指定節點全部後續節點當中,ThreadLocal被回收的節點
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 獲取entry當中的key
        ThreadLocal<?> k = e.get();

        // 若是ThreadLocal爲null,則將value以及數組下標所在位置設置null,方便GC
        // 而且size-1
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {    // 若是不爲null
            // 從新計算key的下標
            int h = k.threadLocalHashCode & (len - 1);

            // 若是是當前位置則遍歷下一個
            // 不是當前位置,則從新從i開始找到下一個爲null的座標進行賦值
            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;
}
複製代碼

​ 上面的代碼註釋我相信已是寫的很清楚了,這個方法實際上就是從staleSlot開始作一個連續段的清理和rehash操做。

set方法系列

​ 接下來咱們看看set方法,天然就是要將咱們的變量保存進ThreadLocal當中,實際上就是保存到ThreadLocalMap當中去,在這裏咱們同樣要思考幾個問題。

​ 1)若是該ThreadLocal對應的ThreadLocalMap還不存在,要怎麼處理?

​ 2)若是所計算的下標,在table當中已經存在Entry節點了怎麼辦?

​ 我想經過上面部分代碼的講解,對這兩個問題,你們也都比較有思路了吧。

​ 老規矩,接下來看看代碼實現

/** * ThreadLocalMap的set方法,這個方法仍是挺關鍵的 * 經過這個方法,咱們能夠看出該哈希表是用線性探測法來解決衝突的 */
private void set(ThreadLocal<?> key, Object value) {

    // 新開一個引用指向table
    Entry[] tab = table;

    // 獲取table的長度
    int len = tab.length;

    // 獲取對應ThreadLocal在table當中的下標
    int i = key.threadLocalHashCode & (len-1);

    /** * 從該下標開始循環遍歷 * 一、如遇相同key,則直接替換value * 二、若是該key已經被回收失效,則替換該失效的key */
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

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

        // 若是 k 爲null,則替換當前失效的k所在Entry節點
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 找到空的位置,建立Entry對象並插入
    tab[i] = new Entry(key, value);

    // table內元素size自增
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    // 新開一個引用指向table
    Entry[] tab = table;

    // 獲取table的長度
    int len = tab.length;
    Entry e;

    // 記錄當前失效的節點下標
    int slotToExpunge = staleSlot;

    /** * 經過這個for循環的prevIndex(staleSlot, len)能夠看出 * 這是由staleSlot下標開始向前掃描 * 查找並記錄最前位置value爲null的下標 */
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    /** * 經過for循環nextIndex(staleSlot, len)能夠看出 * 這是由staleSlot下標開始向後掃描 */
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {

        // 獲取Entry節點對應的ThreadLocal對象
        ThreadLocal<?> k = e.get();

        /** * 若是與新的key對應,直接賦值value * 則直接替換i與staleSlot兩個下標 */
        if (k == key) {
            e.value = value;

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

            // Start expunge at preceding stale entry if it exists
            // 經過註釋看出,i以前的節點裏,沒有value爲null的狀況
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;

            /** * 在調用cleanSomeSlots進行啓發式清理以前 * 會先調用expungeStaleEntry方法從slotToExpunge到table下標所在爲null的連續段進行一次清理 * 返回值即是table[]爲null的下標 * 而後以該下標--len進行一次啓發式清理 * 最終裏面的方法實際上仍是調用了expungeStaleEntry * 能夠看出expungeStaleEntry方法是ThreadLocal核心的清理函數 */
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        /** * 若是當前下標所在已經失效,而且向後掃描過程中沒有找到失效的Entry節點 * 則slotToExpunge賦值爲當前位置 */
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 若是並無在table當中找到該key,則直接在當前位置new一個Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    /** * 在上面的for循環探測過程中 * 若是發現任何無效的Entry節點,則slotToExpunge會被從新賦值 * 就會觸發連續段清理和啓發式清理 */
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}


/** * 啓發式地清理被回收的Entry * i對應的Entry是非無效的,有多是失效被回收了,也有多是null * 會有兩個地方調用到這個方法 * 一、set方法,在判斷是否須要resize以前,會清理並rehash一遍 * 二、替換失效的節點時候,也會進行一次清理 */
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];
        // Entry對象不爲空,可是ThreadLocal這個key已經爲null
        if (e != null && e.get() == null) {
            n = len;
            removed = true;

            /** * 調用該方法進行回收 * 實際上不是隻回收 i 這一個節點而已 * 而是對 i 開始到table所在下標爲null的範圍內,對那些節點都進行一次清理和rehash */
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}


/** * 對table進行擴容,由於要保證table的長度是2的冪,因此擴容就擴大2倍 */
private void resize() {

    // 獲取舊table的長度,而且建立一個長度爲舊長度2倍的Entry數組
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];

    // 記錄插入的有效Entry節點數
    int count = 0;

    /** * 從下標0開始,逐個向後遍歷插入到新的table當中 * 一、如遇到key已經爲null,則value設置null,方便GC回收 * 二、經過hashcode & len - 1計算下標,若是該位置已經有Entry數組,則經過線性探測向後探測插入 */
    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
    size = count;

    // 指向新的Entry數組
    table = newTab;
}
複製代碼

​ 以上的代碼就是調用set方法往ThreadLocalMap當中保存K-V關係的一系列代碼,我就不分開再一個個講了,這樣你們看起來估計也比較方便,有連續性。

​ 咱們能夠來看看一整個的set流程:

​ 一、先經過hashcode & (len - 1)來定位該ThreadLocal在table當中的下標

​ 二、for循環向後遍歷

​ 1)若是獲取Entry節點的key與咱們須要操做的ThreadLocal相等,則直接替換value

​ 2)若是遍歷的時候拿到了key爲null的狀況,則調用replaceStaleEntry方法進行與之替換。

​ 三、若是上述兩個狀況都是,則直接在計算的出來的下標當中new一個Entry階段插入。

​ 四、進行一次啓發式地清理而且若是插入節點後的size大於擴容的閾值,則調用resize方法進行擴容。

remove方法

​ 既然是Map形式進行存儲,咱們有put方法,那確定就會有remove的時候,任何一種數據結構,確定都得符合增刪改查的。

​ 咱們直接來看看代碼。

/** * Remove the entry for key. * 將ThreadLocal對象對應的Entry節點從table當中刪除 */
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) {
            // 將引用設置null,方便GC
            e.clear();

            // 從該位置開始進行一次連續段清理
            expungeStaleEntry(i);
            return;
        }
    }
}
複製代碼

咱們能夠看到,remove節點的時候,也會使用線性探測的方式,當找到對應key的時候,就會調用clear將引用指向null,而且會觸發一次連續段清理。

我相信經過以上對ThreadLocalMap的源碼分析,已經讓你們對其有了個基本的概念認識,相信對你們理解ThreadLocal這個概念的時候,已經不是停留在知道它就是爲了實現線程本地變量而已了。

那接下來咱們來看看ThreadLocal的源碼分析吧。

ThreadLocal源碼分析

ThreadLocal的源碼相對於來講就簡單不少了,由於主要都是ThreadLocalMap這個內部類在幹活,在管理咱們的本地變量。

get方法系列

/** * 獲取當前線程本地變量的值 */
public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程對應的ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    // 若是map不爲空
    if (map != null) {

        // 若是當前ThreadLocal對象對應的Entry還存在
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 而且Entry不爲null,返回對應的值,不然都執行setInitialValue方法
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 若是該線程對應的ThreadLocalMap還不存在,則執行初始化方法
    return setInitialValue();
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


private T setInitialValue() {
    // 獲取初始值,通常是子類重寫
    T value = initialValue();

    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程對應的ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    // 若是map不爲null
    if (map != null)

        // 調用ThreadLocalMap的set方法進行賦值
        map.set(this, value);

    // 不然建立個ThreadLocalMap進行賦值
    else
        createMap(t, value);
    return value;
}


/** * 構造參數建立一個ThreadLocalMap代碼 * ThreadLocal爲key,咱們的泛型爲value */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table的大小爲16
    table = new Entry[INITIAL_CAPACITY];

    // 經過hashcode & (長度-1)的位運算,肯定鍵值對的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    // 建立一個新節點保存在table當中
    table[i] = new Entry(firstKey, firstValue);

    // 設置table內元素爲1
    size = 1;

    // 設置擴容閾值
    setThreshold(INITIAL_CAPACITY);
}
複製代碼

ThreadLocal的get方法也不難,就幾行代碼,可是當它結合了ThreadLocalMap的方法後,這整個邏輯就值得咱們深刻研究寫這個工具的人的思惟了。

​ 咱們來看看它的一個流程吧。

​ 一、獲取當前線程,根據當前線程獲取對應的ThreadLocalMap

​ 二、在ThreadLocalMap當中獲取該ThreadLocal對象對應的Entry節點,而且返回對應的值

​ 三、若是獲取到的ThreadLocalMap爲null,則證實尚未初始化,就調用setInitialValue方法

​ 1)在調用setInitialValue方法的時候,會雙重保證,再進行獲取一次ThreadLocalMap

​ 2)若是依然爲null,就最終調用ThreadLocalMap的構造方法

set方法系列

在這裏我也不對ThreadLocal的set方法作太多介紹了,結合上面的ThreadLocalMap的set方法,我想就能夠對上面每一個方法思考出的問題有個大概的答案。

public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取線程所對應的ThreadLocalMap,從這能夠看出每一個線程都是獨立的
    ThreadLocalMap map = getMap(t);

    // 若是map不爲空,則k-v賦值,看出k是this,也就是當前ThreaLocal對象
    if (map != null)
        map.set(this, value);

    // 若是獲取的map爲空,則建立一個並保存k-v關係
    else
        createMap(t, value);
}
複製代碼

其實ThreadLocal的set方法很簡單的,最主要的都是調用了ThreadLocalMap的set方法,裏面纔是真正核心的執行流程。

不過咱們照樣來看看這個流程:

一、獲取當前線程,根據當前線程獲取對應的ThreadLocalMap

二、若是對應的ThreadLocalMap不爲null,則調用其的set方法保存對應關係

三、若是map爲null,就最終調用ThreadLocalMap的構造方法建立一個ThreadLocalMap並保存對應關係

執行流程總結

在這裏插入圖片描述

源碼分析總結

​ 上面經過對ThreadLocalThreadLocalMap兩個類的源碼進行了分析,我想對於ThreadLocal這個功能的一整個流程,你們都有了個比較清楚的瞭解了。我真的是很佩服==Josh Bloch and Doug Lea==這兩位大神,他們在實現這個東西的時候,不是說光實現了就能夠了,考慮了不少狀況,例如:GC問題、如何維護好數據存儲的問題以及線程與本地變量之間應該以何種方式創建對應關係。

​ 他們寫的代碼邏輯很是之嚴謹,看到這區區幾百行的代碼,才真正地發現,咱們其實主要不是在技術上與別人的差距,而是在功能實現的一整套思惟邏輯上面就與他們有着巨大的差距,最明顯的一點就是,咱們單純是爲了實現而實現,基本上不會考慮其餘異常狀況,更加不會考慮到一些GC問題。

​ 因此經過該篇源碼的分析,讓我真正地意識到,咱們不能光是看源碼作翻譯而已,咱們必定要學會他們是如何思考實現這麼個功能,咱們要學會他們思考每個功能的邏輯。

相關文章
相關標籤/搜索