1. 程式人生 > 實用技巧 >轉 :ThreadLocal系列(一)-ThreadLocal的使用及原理解析

轉 :ThreadLocal系列(一)-ThreadLocal的使用及原理解析

轉:https://www.cnblogs.com/hama1993/p/10382523.html

專案中我們如果想要某個物件在程式執行中的任意位置獲取到,就需要藉助ThreadLocal來實現,這個物件稱作執行緒的本地變數,下面就介紹下ThreadLocal是如何做到執行緒內本地變數傳遞的,

一、基本使用

先來看下基本用法:

private static ThreadLocal tl = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        tl.set(1);
        System.out.println(String.format(
"當前執行緒名稱: %s, main方法內獲取執行緒內資料為: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(ThreadLocalTest::fc).start(); } private static void fc() { System.out.println(String.format("當前執行緒名稱: %s, fc方法內獲取執行緒內資料為: %s", Thread.currentThread().getName(), tl.get())); }

執行結果:

當前執行緒名稱: main, main方法內獲取執行緒內資料為: 1
當前執行緒名稱: main, fc方法內獲取執行緒內資料為: 1
當前執行緒名稱: Thread-0, fc方法內獲取執行緒內資料為: null

可以看到,main執行緒內任意地方都可以通過ThreadLocal獲取到當前執行緒內被設定進去的值,而被異步出去的fc呼叫,卻由於替換了執行執行緒,而拿不到任何資料值,那麼我們現在再來改造下上述程式碼,在非同步發生之前,給Thread-0執行緒也設定一個上下文資料:

private static ThreadLocal tl = new ThreadLocal<>();

    
public static void main(String[] args) throws Exception { tl.set(1); System.out.println(String.format("當前執行緒名稱: %s, main方法內獲取執行緒內資料為: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(()->{ tl.set(2); //在子執行緒裡設定上下文內容為2 fc(); }).start(); Thread.sleep(1000L); //保證下面fc執行一定在上面非同步程式碼之後執行 fc(); //繼續在主執行緒內執行,驗證上面那一步是否對主執行緒上下文內容造成影響 } private static void fc() { System.out.println(String.format("當前執行緒名稱: %s, fc方法內獲取執行緒內資料為: %s", Thread.currentThread().getName(), tl.get())); }

執行結果為:

當前執行緒名稱: main, main方法內獲取執行緒內資料為: 1 
當前執行緒名稱: main, fc方法內獲取執行緒內資料為: 1
當前執行緒名稱: Thread-0, fc方法內獲取執行緒內資料為: 2
當前執行緒名稱: main, fc方法內獲取執行緒內資料為: 1

可以看到,主執行緒和子執行緒都可以獲取到自己的那份上下文裡的內容,而且互不影響。

二、原理分析

ok,上面通過一個簡單的例子,我們可以瞭解到ThreadLocal(以下簡稱TL)具體的用法,這裡先不討論它實質上能給我們帶來什麼好處,先看看其實現原理,等這些差不多瞭解完了,我再通過我曾經做過的一個專案,去說明TL的作用以及在企業級專案裡的用處。

我以前在不瞭解TL的時候,想著如果讓自己實現一個這種功能的輪子,自己會怎麼做,那時候的想法很單純,覺得通過一個Map就可以解決,Map的key設定為Thread.currentThread(),value設定為當前執行緒的本地變數即可,但後來想想就覺得不太現實了,實際專案中可能存在大量的非同步執行緒,對於記憶體的開銷是不可估量的,而且還有個嚴重的問題,執行緒是執行結束後就銷燬的,如果按照上述的實現方案,map內是一直持有這個執行緒的引用的,導致明明執行結束的執行緒物件不能被jvm回收,造成記憶體洩漏,時間久了,會直接OOM。

所以,java裡的實現肯定不是這麼簡單的,下面,就來看看java裡的具體實現吧。

先來了解下,TL的基本實現,為了避免上述中出現的問題,TL實際上是把我們設定進去的值以k-v的方式放到了每個Thread物件內(TL物件做k,設定的值做v),也就是說,TL物件僅僅起到一個標記、對Thread物件維護的map賦值的作用。

先從set方法看起:

public void set(T value) {
        Thread t = Thread.currentThread(); //獲取當前執行緒
        ThreadLocal.ThreadLocalMap map = getMap(t); //獲取到當前執行緒持有的ThreadLocalMap物件
        if (map != null)
            map.set(this, value); //直接set值,具體方法在下面
        else
            createMap(t, value); // 為空就給當前執行緒建立一個ThreadLocalMap物件,賦值給Thread物件,具體方法在下面
    }

    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; //每個執行緒都有一個ThreadLocalMap,key為TL物件(其實是根據物件hash計算出來的值),value為該執行緒在此TL物件下儲存的內容值
    }

    private void set(ThreadLocal<?> key, Object value) {

        ThreadLocal.ThreadLocalMap.Entry[] tab = table; //獲取儲存k-v物件的陣列(散列表)
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1); //根據TL物件的hashCode(也是特殊計算出來的,保證每個TL物件的hashCode不同)計算出下標

        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) { //線性探查法解決雜湊衝突問題,發現下標i已經有Entry了,則就檢視i+1位置處是否有值,以此類推
            ThreadLocal<?> k = e.get(); //獲取k

            if (k == key) { //若k就是當前TL物件,則直接為其value賦值
                e.value = value;
                return;
            }

            if (k == null) { //若k為空,則認為是可回收的Entry,則利用當前k和value組成新的Entry替換掉該可回收Entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        //for迴圈執行完沒有終止程式,說明遇到了空槽,這個時候直接new物件賦值即可
        tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold) //這裡用來清理掉k為null的廢棄Entry
            rehash(); //如果沒有發生清除Entry並且size超過閾值(閾值 = 最大長度 * 2/3),則進行擴容
    }


    //直接為當前Thread初始化它的ThreadLocalMap物件
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY]; //初始化陣列
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //計算初始位置
        table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue); //因為初始化不存在hash衝突,直接new
        size = 1;
        setThreshold(INITIAL_CAPACITY); //給閾值賦值,上面已經提及,閾值 = 最大長度 * 2/3
    }

通過上述程式碼,我們大致瞭解了TL在set值的時候發生的一些操作,結合之前說的,我們可以確定的是,TL其實對於執行緒來說,只是一個標識,而真正執行緒的本地變數被儲存在每個執行緒物件的ThreadLocalMap裡,這個map裡維護著一個Entry[]的陣列(散列表),Entry是個k-v結構的物件(如圖1-1),k為TL物件,v為對應TL儲存在該執行緒內的本地變數值,值得注意的是,這裡的k針對TL物件的引用是個弱引用,來看下原始碼:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

為什麼這裡需要弱引用呢?我們先來看一張圖,結合上面的介紹和這張圖,來了解TL和Thread間的關係:

圖1-1

圖中虛線表示弱引用,那麼為什麼要這麼做呢?

簡單來說,一個TL物件被創建出來,並且被一個執行緒放到自己的ThreadLocalMap裡,假如TL物件失去原有的強引用,但是該執行緒還沒有死亡,如果k不是弱引用,那麼就意味著TL並不能被回收,現在k為弱引用,那麼在TL失去強引用的時候,gc可以直接回收掉它,弱引用失效,這就是上面程式碼裡會進行檢查,k=null的清除釋放記憶體的原因(這個可以參考下面expungeStaleEntry方法,而且set、get、remove都會呼叫該方法,這也是TL防止記憶體洩漏所做的處理)。

綜上,簡單來說這個弱引用就是用來解決由於使用TL不當導致的記憶體洩漏問題的,假如沒有弱引用,那麼你又用到了執行緒池(池化後執行緒不會被銷燬),然後TL物件又是區域性的,那麼就會導致執行緒池內執行緒裡的ThreadLocalMap存在大量的無意義的TL物件引用,造成過多無意義的Entry物件,因為即便呼叫了set、get等方法檢查k=null,也沒有作用,這就導致了記憶體洩漏,長時間這樣最終可能導致OOM,所以TL的開發者為了解決這種問題,就將ThreadLocalMap裡對TL物件的引用改為弱引用,一旦TL物件失去強引用,TL物件就會被回收,那麼這裡的弱引用指向的值就為null,結合上面說的,呼叫操作方法時會檢查k=null的Entry進行回收,從而避免了記憶體洩漏的可能性。

因為TL解決了記憶體洩漏的問題,因此即便是區域性變數的TL物件且啟用執行緒池技術,也比較難造成記憶體洩漏的問題,而且我們經常使用的場景就像一開始的示例程式碼一樣,會初始化一個全域性的static的TL物件,這就意味著該物件在程式執行期間都不會存在強引用消失的情況,我們可以利用不同的TL物件給不同的Thread裡的ThreadLocalMap賦值,通常會set值(覆蓋原有值),因此在使用執行緒池的時候也不會造成問題,非同步開始之前set值,用完以後remove,TL物件可以多次得到使用,啟用執行緒池的情況下如果不這樣做,很可能業務邏輯也會出問題(一個執行緒存在之前執行程式時遺留下來的本地變數,一旦這個執行緒被再次利用,get時就會拿到之前的髒值);

說完了set,我們再來看下get:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = getMap(t); //獲取執行緒內的ThreadLocalMap物件
        if (map != null) {
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); //根據當前TL物件(key)獲取對應的Entry
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result; //直接返回value即可
            }
        }
        return setInitialValue(); //如果發現當前執行緒還沒有ThreadLocalMap物件,則進行初始化
    }

    private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1); //計算下標
        ThreadLocal.ThreadLocalMap.Entry e = table[i];
        if (e != null && e.get() == key) //根據下標獲取的Entry物件如果key也等於當前TL物件,則直接返回結果即可
            return e;
        else
            return getEntryAfterMiss(key, i, e); //上面說過,有些情況下存在下標衝突的問題,TL是通過線性探查法來解決的,所以這裡也一樣,如果上面沒找到,則繼續通過下標累加的方式繼續尋找
    }

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

        while (e != null) {
            ThreadLocal<?> k = e.get(); //繼續累加下標的方式一點點的往下找
            if (k == key) //找到了就返回出去結果
                return e;
            if (k == null) //這裡也會檢查k==null的Entry,滿足就執行刪除操作
                expungeStaleEntry(i);
            else //否則繼續累加下標查詢
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null; //找不到返回null
    }


    //這裡也放一下nextIndex方法
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

最後再來看看remove方法:

public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this); //清除掉當前執行緒ThreadLocalMap裡以當前TL物件為key的Entry
    }

    private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1); //計算下標
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) { //找到目標Entry
                e.clear(); //清除弱引用
                expungeStaleEntry(i); //通過該方法將自己清除
                return;
            }
        }
    }

    private int expungeStaleEntry(int staleSlot) { //引數為目標下標
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;

        tab[staleSlot].value = null; //首先將目標value清除
        tab[staleSlot] = null;
        size--;

        // Rehash until we encounter null
        ThreadLocal.ThreadLocalMap.Entry e;
        int i;
        // 由目標下標開始往後逐個檢查,k==null的清除掉,不等於null的要進行rehash
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) { 
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                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.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }

目前主要方法set、get、remove已經介紹完了,包含其內部存在的弱引用的作用,以及實際專案中建議的用法,以及為什麼要這樣用,也進行了簡要的說明,下面一篇會進行介紹InheritableThreadLocal的用法以及其原理性分析。