ThreadLocal和ThreadLocalMap原始碼分析

KingJack發表於2019-03-26

背景分析

​ 相信很多程式猿在平常實現功能的過程當中,都會遇到想要某些靜態變數,不管是單執行緒亦或者是多執行緒在使用,都不會產生相互之間的影響,也就是這個靜態變數線上程之間是讀寫隔離的。

​ 有一個我們經常使用的工具類,它的併發問題就是用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方法能夠獲取和賦值。

​ 1、那我們賦值的內容,究竟儲存在什麼結構當中?

​ 2、它究竟是怎麼做到執行緒隔離的?

​ 3、當我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物件,我們來看看它的流程

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

​ 2、如果獲取到直接返回,沒獲取到的話,就接著往後遍歷看是否能獲取到(因為用的是線性探測法,往後遍歷有可能獲取到結果)

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

expungeStaleEntry方法

​ 這可以說是ThreadLocal非常核心的一個清理方法,為什麼會需要清理呢?或許很多人想不明白,我們用List或者是Map也好,都沒有說要清理裡面的內容。

​ 但是這裡是對於執行緒來說的隔離的本地變數,並且使用的是弱引用,那便有可能在GC的時候就被回收了。

​ 1)如果有很多Entry節點已經被回收了,但是在table陣列中還留著位置,這時候不清理就會浪費資源

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

​ 我們來看看別人原始碼是怎麼做的吧!

/**        
 * 這個函式可以看做是ThreadLocal裡的核心清理函式,它主要做的事情就是
 * 1、從staleSlot開始,向後遍歷將ThreadLocal物件被回收所在Entry節點的value和Entry節點本身設定null,方便GC,並且size自減1
 * 2、並且會對非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);

    /**
     * 從該下標開始迴圈遍歷
     * 1、如遇相同key,則直接替換value
     * 2、如果該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
 * 會有兩個地方呼叫到這個方法
 * 1、set方法,在判斷是否需要resize之前,會清理並rehash一遍
 * 2、替換失效的節點時候,也會進行一次清理
 */
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當中
     * 1、如遇到key已經為null,則value設定null,方便GC回收
     * 2、通過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流程:

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

​ 2、for迴圈向後遍歷

​ 1)如果獲取Entry節點的key與我們需要操作的ThreadLocal相等,則直接替換value

​ 2)如果遍歷的時候拿到了key為null的情況,則呼叫replaceStaleEntry方法進行與之替換。

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

​ 4、進行一次啟發式地清理並且如果插入節點後的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的方法後,這整個邏輯就值得我們深入研究寫這個工具的人的思維了。

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

​ 1、獲取當前執行緒,根據當前執行緒獲取對應的ThreadLocalMap

​ 2、在ThreadLocalMap當中獲取該ThreadLocal物件對應的Entry節點,並且返回對應的值

​ 3、如果獲取到的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方法,裡面才是真正核心的執行流程。

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

1、獲取當前執行緒,根據當前執行緒獲取對應的ThreadLocalMap

2、如果對應的ThreadLocalMap不為null,則呼叫其的set方法儲存對應關係

3、如果map為null,就最終呼叫ThreadLocalMap的構造方法建立一個ThreadLocalMap並儲存對應關係

執行流程總結

在這裡插入圖片描述

原始碼分析總結

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

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

​ 所以通過該篇原始碼的分析,讓我真正地意識到,我們不能光是看原始碼做翻譯而已,我們一定要學會他們是如何思考實現這麼個功能,我們要學會他們思考每一個功能的邏輯。

相關文章