揭開神秘面紗——深入淺出ThreadLocal

jerrysun發表於2021-09-09

能夠找到這篇文章,說明你已開始學習Java的多執行緒了,也瞭解多執行緒的同步、鎖等概念。但,ThreadLocal雖出現在多執行緒的環境中,對於它的使用,並不涉及到鎖和同步的概念。它生於多執行緒,伴隨著多執行緒的熱點,而並不沾染多執行緒的常見問題,是不是莫名的小清新呢?如果你對它有所瞭解,聽說過記憶體洩露,如何才能更好的駕馭它呢?帶著好奇和疑惑,一起深入ThreadLocal吧!

1.背景

隨便舉兩個具體的例子:

  1. 在一個web專案中,從請求一進來就為之生成一個 uuid,無論系統是否報異常,返回給客戶的必須是同一 uuid,可能首先會想到當作方法的引數來傳遞,這樣任何地方可以成功的獲取到這個 uuid,但這個 uuid 會在系統中幾乎各個方法引數中都會出現,但 uuid 又非主要業務引數,這樣勢必會與業務耦合性太強。

  2. 對於很多非執行緒安全的類而言,如工具類:SimpleDateFormat和JDBC的Connection,它們經常出現在併發環境中,例如Connection,大家剛接觸JDBC的時候,都是在方法中完成Connection的 init/commit/close,如多個執行緒都想連線資料庫執行sql,方法有如下:

    1. 對Connection進行同步加鎖,協調各個執行緒操作DB的順序,沒錯但很低效。

    2. 每個執行緒自己建立Connection,會造成頻繁的建立和釋放連線,執行緒結束,Connection也就結束。

2. 與併發/同步的區別

什麼,你突然想到了併發中的同步?先立一個flag,其實他們有本質的區別,同步是協調多個執行緒對同一個變數的修改,而ThreadLocal則是將這個變數的副本據為執行緒己有,各個執行緒操作的是各自的threadlocal變數,各個執行緒互不影響,自然不會涉及到同步。

3. 易混名詞釋疑

大家容易搞混Thread/ThreadLocal/ThreadLocalMap三者的關係,其實很簡單,如圖:

圖片描述

關係圖

為了便於大家記住依賴關係,煞費苦心的我編了個故事:從前,有一個雷劈出來一個執行緒小天(Thread),有一天,遇到了小樂(threadLocal),執行緒小天說:“如果想發揮價值,你必須初始化一個ThreadLocalMap放我這託管!” 小樂呼叫initialValue()不一會兒就初始化了ThreaLocalMap——小明(ThreadLocal的靜態內部類),然後樂轉身給天說,這是小明,小天看著歡喜地說:“明明,我給你一個小名threadLocals吧(將其賦值給當前執行緒的threadLocals變數),以後呢,你就跟我小天混,只要我還在,你就會有肉吃。你的工作內容也很簡單,如果以後小樂呼叫get()方法獲取值的時候,你就將他的 threadLocalHashCode 作為key,在你的Map中找到對應的value。當然 set() 方法也差不多,頂多處理一下hash衝突的問題,不過這是你的內務,我就不干預了。”  當然,小天后面也有遇到其他的ThreadLocal,不過它已經有Map小明瞭,直接讓小明幹活就可以了。

4. 原始碼時間

作為專業的看官,等的就是程式碼,靜下心來,15min後,讓你感受到鹹魚翻身,雖然還是鹹魚,哈哈

來看ThreadLocal這個類,其中包括 get/set/remove 等方法,為了避免碼字嫌疑,只貼關鍵程式碼(其中加入了筆者的中文註釋,幫助理解),下面逐個介紹:

4.1 set()

程式碼包含set()方法,同時包括方法體內所呼叫的其他方法(後同)

    // 程式碼段1
    public class ThreadLocal {        //...
        public void set(T value) {            // 獲取當前執行緒t
            Thread t = Thread.currentThread();            // 獲取當前執行緒對應的 ThreadLocalMap,它是ThreadLocal的一個內部類
            ThreadLocal.ThreadLocalMap map = getMap(t);            // 如果map之前被建立,則直接進map中取值
            if (map != null)
                map.set(this, value);            // 建立ThreadLocalMap
            else
                createMap(t, value);
        }        // 獲取執行緒t的ThreadLocalMap,無則return null
        ThreadLocal.ThreadLocalMap getMap(Thread t) {            return t.threadLocals;
        }        // 建立執行緒t的ThreadLocalMap,並設定初始值
        void createMap(Thread t, T firstValue) {            // 建立新的ThreadLocalMap,並將其與當前執行緒進行關聯,構造方法往下翻
            t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
        }        static class ThreadLocalMap {            //...
            /**
             * Construct a new map initially containing (firstKey, firstValue).
             * ThreadLocalMaps are constructed lazily, so we only create
             * one when wse have at least one entry to put in it.
             */
            ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {                // 初始化table,INITIAL_CAPACITY為16
                table = new Entry[INITIAL_CAPACITY];                // 將threadLocal的threadLocalHashCode除以16取模(下面這種騷操作是因為除數是2^n),得到桶的下標
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);                // 將生成的Entry 放置於table對應的桶中
                table[i] = new Entry(firstKey, firstValue);
                size = 1;                // 設定擴容閾值為table length的 2/3(負載因子2/3)
                setThreshold(INITIAL_CAPACITY);
            }            private void setThreshold(int len) {
                threshold = len * 2 / 3;
            }            
            private void set(ThreadLocal> key, Object value) {                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.

                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) {
                        replaceStaleEntry(key, value, i);                        return;
                    }
                }

                tab[i] = new Entry(key, value);                int sz = ++size;                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }            //...
        }        private final int threadLocalHashCode = nextHashCode();        private static AtomicInteger nextHashCode = new AtomicInteger();        /**
         * The difference between successively generated hash codes - turns
         * implicit sequential thread-local IDs into near-optimally spread
         * multiplicative hash values for power-of-two-sized tables.
         * 這個數字就不得不提了,以他為步長生成的2^n個數字序列,
         * 除以2^n取模後,得到的模居然可以逐個均勻的落在2^n個桶中,
         * 與傳統步長(+1)的不同在於,逐個均勻分佈(而非連續分佈)可以減小碰撞的機率,
         * (可以拿著這個數應用在其他類似場景中)
         */
        private static final int HASH_INCREMENT = 0x61c88647;        private static int nextHashCode() {            // 從0開始遞增
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }        //...
    }
// 程式碼段2public class Thread implements Runnable {//...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;//...}

整體流程可以看出,當呼叫set(T value)方法時,會先取出本執行緒的ThreadLocalMap,對於Map:

  • 如果不為空,則以ThreadLocal例項為key, 將value儲存在此Map中

  • 如果為空,就建立一個Map,並將其賦值給此執行緒的成員變數threadLocals

對於ThreadLocalMap是由誰來維護,其定義的程式碼如下:

// 程式碼段3public class ThreadLocal {//...
    static class ThreadLocalMap {        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference {            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {                super(k);
                value = v;
            }
        }        //...
    }//...}

結合程式碼段2,可以看出,ThreadLocalMap其實是定義在ThreadLocal中的靜態內部類,然後由Thread類來維護,依附於Thread的生命週期。讀過HashMap原始碼的童鞋知道Entry是什麼東東,這個Entry 繼承了 WeakReference類,其實就Entry的key繼承了它,從建構函式就可以看出,順帶簡單回顧下java 的引用:

  • 強引用:不受GC影響,即時OOM也不回收;eg. Person p = new Person("Norman")

  • 軟引用:只會在記憶體不足時,由GC回收;

  • 弱引用:不論記憶體是否夠用,一旦GC,則回收,不過GC的執行緒優先順序,不一定很快的發現;

  • 虛引用:形同虛設,與前三不同,它必須配合WeakReferenceQueue,跟蹤物件被垃圾回收的活動

那麼,為什麼要用到弱引用呢?官方文件如是說:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

譯:為了應對非常大的和長壽命的物件使用,雜湊表 Entry 使用 WeakReferences 作為鍵。但是,由於沒有使用引用佇列(Reference類中的佇列), 因此只有當錶快耗盡空間時, 才保證將陳舊 Entry 刪除。

如下場景很好的解釋了這樣設計的好處(感謝):

強引用: 當物件A中引用ThreadLocal的物件B,A被回收,則B變為垃圾,但執行緒對Map是強引用,Map對B是 引用,只要執行緒存活,則B始終不會被回收。

弱引用: 當物件A中引用ThreadLocal的物件B,A被回收,則B變為垃圾,由於執行緒對Map是強引用,Map對B是 引用,即使沒有手動刪除,在下一個GC週期,B也會被回收掉。而Map中的value會在呼叫set/get/remove方法後斷掉強引用,等待GC後續回收(見 4.4 記憶體洩露)。

4.2 get()

public class ThreadLocal {//...
    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {        // 1.獲取當前執行緒t
        Thread t = Thread.currentThread();        // 2.獲取當前執行緒對應的 ThreadLocalMap, 它是ThreadLocal的一個內部類
        ThreadLocal.ThreadLocalMap map = getMap(t);        // 3.如果map之前被建立,則直接進map中取值
        if (map != null) {            // 3.1 以當前ThreadLocal例項作為key,在map中獲取對應的Entry
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")
                T result = (T)e.value;                return result;
            }
        }        // 4.如果map之前未被建立,或建立後未得到該ThreadLocal例項的Entry
        //   則呼叫此方法進行初始化,而後返回結果
        return setInitialValue();
    }    static class ThreadLocalMap {        //...
        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
                // 如果不在,則按規則尋找
                return getEntryAfterMiss(key, i, e);
        }        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)
                    expungeStaleEntry(i);                else
                    i = nextIndex(i, len);
                e = tab[i];
            }            return null;
        }//...}

如果,當前執行緒沒有threadLocal值,則預設呼叫initialValue()方法,其中的取值可以看到,ThreadLocalMap中處理Hash衝突的方法是線性探測法,順帶回顧下資料結構中,Hash衝突的解決辦法:

  1. 開放地址法

  • 線性探測 (ThreadLocalMap)

  • 二次探測

  • 再雜湊

  • 再雜湊法

  • 鏈地址法 (HashMap)

  • 建立一個公共溢位區

  • 4.3 remove()

        public class ThreadLocal {        // ...
            public void remove() {
                ThreadLocalMap m = getMap(Thread.currentThread());            if (m != null)                // 呼叫ThreadLocalMap的remove方法
                    m.remove(this);
            }        // ...
    
            static class ThreadLocalMap {            // ...
                
                private void remove(ThreadLocal> key) {
                    Entry[] tab = table;                int len = tab.length;                // 按hashcode計算出應該在的位置
                    int i = key.threadLocalHashCode & (len - 1);                // 考慮到hash衝突,按線性探測查詢應該在的位置
                    for (Entry e = tab[i];
                         e != null;
                         e = tab[i = nextIndex(i, len)]) {                    // 判斷是否是目標Entry
                        if (e.get() == key) {                        // 呼叫Reference中的clear()方法,Clears this reference object.
                            e.clear();                        // 將該位置的Entry清除掉後,對table重新整理
                            expungeStaleEntry(i);                        return;
                        }
                    }
                }            private int expungeStaleEntry(int staleSlot) {
                    Entry[] tab = table;                int len = tab.length;                // expunge entry at staleSlot
                    // 先將value引用置空
                    tab[staleSlot].value = null;
                    tab[staleSlot] = null;
                    size--;                // Rehash until we encounter null
                    // 因為後續連續的元素可能是之前hash衝突引起的,
                    // 所以,對table後續連續的元素,進行重新hash
                    Entry e;                int i;                for (i = nextIndex(staleSlot, len);
                         (e = tab[i]) != null;
                         i = nextIndex(i, len)) {
                        ThreadLocal> k = e.get();                    // 如果key為空,順便清除它
                        if (k == null) {
                            e.value = null;
                            tab[i] = null;
                            size--;
                        } else {                        int h = k.threadLocalHashCode & (len - 1);                        // 如果一個元素按照hashcode運算後,
                            // 實際位置不在應該在的位置,則對其重新hash
                            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;
                }
            }
        }

    4.4 記憶體洩露

    從原始碼中看,無論是get(),set()還是remove()操作,都會包含對ThreadLocalMap 中key為null的Entry清除,那麼洩露會出現在什麼地方呢?仔細來看各部分依賴圖:

    圖片描述

    內部關聯

    ThreadLocal可手動置為null,也可以由GC置null(因為弱引用),但這只是針對key,對於value,當前Entry的value被Entry引用,而Entry被當前Map引用,而Map則被當前執行緒例項Thread引用,如果當前執行緒不退出,則value是不會被GC,造成記憶體洩露。

    更加準確的說,是發生在 :當Map中的key(ThreadLocal)為null後到執行緒結束 這期間。當遇到執行緒池,執行緒會被重複利用,如果使用 set 後不再使用 get/set/remove,這個強應用會一直存在,造成記憶體洩露。(PS:當value是大物件時尤為嚴重)

    那補救措施有哪些呢?

    1. 首先,jdk本身get/set/remove操作會清除key為null的Entry,但屬於被動清除,不呼叫此方法,依然會記憶體洩露

    2. 其次,當用完threadLocal後,應該主動呼叫remove方法,主動斷掉value到thread的引用鏈

    5. 總結

    使用ThreadLocal有一些建議:

    1. 使用static修飾,使之屬於類而不是例項,因為它持有的物件,生效範圍一般在使用者會話/web請求週期期間。

      One web request => one Persistence session.
      Not one web request => one persistence session per object.

    2. 如上文提到,使用結束後呼叫remove()方法進行清除,避免造成記憶體洩露。



    作者:NormanHu
    連結:


    來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/151/viewspace-2809671/,如需轉載,請註明出處,否則將追究法律責任。

    相關文章