JAVA併發-自問自答學ThreadLocal

liangzzz發表於2017-11-17

前言

ThreadLocal很多同學都搞不懂是什麼東西,可以用來幹嘛。但面試時卻又經常問到,所以這次我和大家一起學習ThreadLocal這個類。

下面我就以面試問答的形式學習我們的——ThreadLocal類(原始碼分析基於JDK8)

本文同步釋出於簡書 :www.jianshu.com/p/807686414…

問答內容

1.

問:ThreadLocal瞭解嗎?您能給我說說他的主要用途嗎?

答:

  • 從JAVA官方對ThreadLocal類的說明定義(定義在示例程式碼中):ThreadLocal類用來提供執行緒內部的區域性變數。這種變數在多執行緒環境下訪問(通過getset方法訪問)時能保證各個執行緒的變數相對獨立於其他執行緒內的變數。ThreadLocal例項通常來說都是private static型別的,用於關聯執行緒和執行緒上下文。

  • 我們可以得知ThreadLocal的作用是:ThreadLocal的作用是提供執行緒內的區域性變數,不同的執行緒之間不會相互干擾,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或元件之間一些公共變數的傳遞的複雜度。

  • 上述可以概述為:ThreadLocal提供執行緒內部的區域性變數,在本執行緒內隨時隨地可取,隔離其他執行緒。

示例程式碼:


/**
 * 該類提供了執行緒區域性 (thread-local) 變數。 這些變數不同於它們的普通對應物,
 * 因為訪問某個變數(通過其 get 或 set 方法)的每個執行緒都有自己的區域性變數
 * 它獨立於變數的初始化副本。ThreadLocal 例項通常是類中的 private static 欄位
 * 它們希望將狀態與某一個執行緒(例如,使用者 ID 或事務 ID)相關聯。
 *
 * 例如,以下類生成對每個執行緒唯一的區域性識別符號。
 * 
 * 執行緒 ID 是在第一次呼叫 UniqueThreadIdGenerator.getCurrentThreadId() 時分配的,
 * 在後續呼叫中不會更改。
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // 原子性整數,包含下一個分配的執行緒Thread ID 
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // 每一個執行緒對應的Thread ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // 返回當前執行緒對應的唯一Thread ID, 必要時會進行分配
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * 每個執行緒都保持對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的
 * 線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收,(除非存在對這些副本的其他引用)。
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
·····
   /**
     * 自定義雜湊碼(僅在ThreadLocalMaps中有用)
     * 可用於降低hash衝突
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 生成下一個雜湊碼hashCode. 生成操作是原子性的. 從0開始
     * 
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();


    /**
     * 表示了連續分配的兩個ThreadLocal例項的threadLocalHashCode值的增量 
     */
    private static final int HASH_INCREMENT = 0x61c88647;


    /**
     * 返回下一個雜湊碼hashCode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
·····

}複製程式碼
  • 其中nextHashCode()方法就是一個原子類不停地去加上0x61c88647,這是一個很特別的數,叫斐波那契雜湊(Fibonacci Hashing),斐波那契又有一個名稱叫黃金分割,也就是說將這個數作為雜湊值的增量將會使雜湊表的分佈更為均勻。

2.

問:ThreadLocal實現原理是什麼,它是怎麼樣做到區域性變數不同的執行緒之間不會相互干擾的?

答:

  • 通常,如果我不去看原始碼的話,我猜ThreadLocal是這樣子設計的:每個ThreadLocal類都建立一個Map,然後用執行緒的ID threadID作為Mapkey,要儲存的區域性變數作為Mapvalue,這樣就能達到各個執行緒的值隔離的效果。這是最簡單的設計方法,JDK最早期的ThreadLocal就是這樣設計的。

  • 但是,JDK後面優化了設計方案,現時JDK8 ThreadLocal的設計是:每個Thread維護一個ThreadLocalMap雜湊表,這個雜湊表的keyThreadLocal例項本身,value才是真正要儲存的值Object

  • 這個設計與我們一開始說的設計剛好相反,這樣設計有如下幾點優勢:

    1) 這樣設計之後每個Map儲存的Entry數量就會變小,因為之前的儲存數量由Thread的數量決定,現在是由ThreadLocal的數量決定。

    2) 當Thread銷燬之後,對應的ThreadLocalMap也會隨之銷燬,能減少記憶體的使用。

ThreadLocal引用關係圖- 圖片來自於《簡書 - 對ThreadLocal實現原理的一點思考》
ThreadLocal引用關係圖- 圖片來自於《簡書 - 對ThreadLocal實現原理的一點思考》

上述解釋主要參考自:ThreadLocal和synchronized的區別?

3.

問:您能說說ThreadLocal常用操作的底層實現原理嗎?如儲存set(T value),獲取get(),刪除remove()等操作。

答:

  • 呼叫get()操作獲取ThreadLocal中對應當前執行緒儲存的值時,進行了如下操作:

    1 ) 獲取當前執行緒Thread物件,進而獲取此執行緒物件中維護的ThreadLocalMap物件。

    2 ) 判斷當前的ThreadLocalMap是否存在:

  • 如果存在,則以當前的ThreadLocalkey,呼叫ThreadLocalMap中的getEntry方法獲取對應的儲存實體 e。找到對應的儲存實體 e,獲取儲存實體 e 對應的 value值,即為我們想要的當前執行緒對應此ThreadLocal的值,返回結果值。
  • 如果不存在,則證明此執行緒沒有維護的ThreadLocalMap物件,呼叫setInitialValue方法進行初始化。返回setInitialValue初始化的值。

  • setInitialValue方法的操作如下:

    1 ) 呼叫initialValue獲取初始化的值。

    2 ) 獲取當前執行緒Thread物件,進而獲取此執行緒物件中維護的ThreadLocalMap物件。

    3 ) 判斷當前的ThreadLocalMap是否存在:

  • 如果存在,則呼叫map.set設定此實體entry

  • 如果不存在,則呼叫createMap進行ThreadLocalMap物件的初始化,並將此實體entry作為第一個值存放至ThreadLocalMap中。

PS:關於ThreadLocalMap對應的相關操作,放在下一個問題詳細說明。

示例程式碼:

    /**
     * 返回當前執行緒對應的ThreadLocal的初始值
     * 此方法的第一次呼叫發生在,當執行緒通過{@link #get}方法訪問此執行緒的ThreadLocal值時
     * 除非執行緒先呼叫了 {@link #set}方法,在這種情況下,
     * {@code initialValue} 才不會被這個執行緒呼叫。
     * 通常情況下,每個執行緒最多呼叫一次這個方法,
     * 但也可能再次呼叫,發生在呼叫{@link #remove}方法後,
     * 緊接著呼叫{@link #get}方法。
     *
     * <p>這個方法僅僅簡單的返回null {@code null};
     * 如果程式設計師想ThreadLocal執行緒區域性變數有一個除null以外的初始值,
     * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
     * 通常, 可以通過匿名內部類的方式實現
     *
     * @return 當前ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }

    /**
     * 建立一個ThreadLocal
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * 返回當前執行緒中儲存ThreadLocal的值
     * 如果當前執行緒沒有此ThreadLocal變數,
     * 則它會通過呼叫{@link #initialValue} 方法進行初始化值
     *
     * @return 返回當前執行緒對應此ThreadLocal的值
     */
    public T get() {
        // 獲取當前執行緒物件
        Thread t = Thread.currentThread();
        // 獲取此執行緒物件中維護的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以當前的ThreadLocal 為 key,呼叫getEntry獲取對應的儲存實體e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到對應的儲存實體 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 獲取儲存實體 e 對應的 value值
                // 即為我們想要的當前執行緒對應此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,則證明此執行緒沒有維護的ThreadLocalMap物件
        // 呼叫setInitialValue進行初始化
        return setInitialValue();
    }

    /**
     * set的變樣實現,用於初始化值initialValue,
     * 用於代替防止使用者重寫set()方法
     *
     * @return the initial value 初始化後的值
     */
    private T setInitialValue() {
        // 呼叫initialValue獲取初始化的值
        T value = initialValue();
        // 獲取當前執行緒物件
        Thread t = Thread.currentThread();
        // 獲取此執行緒物件中維護的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則呼叫map.set設定此實體entry
            map.set(this, value);
        else
            // 1)當前執行緒Thread 不存在ThreadLocalMap物件
            // 2)則呼叫createMap進行ThreadLocalMap物件的初始化
            // 3)並將此實體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回設定的值value
        return value;
    }

    /**
     * 獲取當前執行緒Thread對應維護的ThreadLocalMap 
     * 
     * @param  t the current thread 當前執行緒
     * @return the map 對應維護的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }複製程式碼
  • 呼叫set(T value)操作設定ThreadLocal中對應當前執行緒要儲存的值時,進行了如下操作:

    1 ) 獲取當前執行緒Thread物件,進而獲取此執行緒物件中維護的ThreadLocalMap物件。

    2 ) 判斷當前的ThreadLocalMap是否存在:

  • 如果存在,則呼叫map.set設定此實體entry

  • 如果不存在,則呼叫createMap進行ThreadLocalMap物件的初始化,並將此實體entry作為第一個值存放至ThreadLocalMap中。

示例程式碼:

    /**
     * 設定當前執行緒對應的ThreadLocal的值
     * 大多數子類都不需要重寫此方法,
     * 只需要重寫 {@link #initialValue}方法代替設定當前執行緒對應的ThreadLocal的值
     *
     * @param value 將要儲存在當前執行緒對應的ThreadLocal的值
     *  
     */
    public void set(T value) {
        // 獲取當前執行緒物件
        Thread t = Thread.currentThread();
        // 獲取此執行緒物件中維護的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則呼叫map.set設定此實體entry
            map.set(this, value);
        else
            // 1)當前執行緒Thread 不存在ThreadLocalMap物件
            // 2)則呼叫createMap進行ThreadLocalMap物件的初始化
            // 3)並將此實體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
    }

    /**
     * 為當前執行緒Thread 建立對應維護的ThreadLocalMap. 
     *
     * @param t the current thread 當前執行緒
     * @param firstValue 第一個要存放的ThreadLocal變數值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }複製程式碼
  • 呼叫remove()操作刪除ThreadLocal中對應當前執行緒已儲存的值時,進行了如下操作:

    1 ) 獲取當前執行緒Thread物件,進而獲取此執行緒物件中維護的ThreadLocalMap物件。

    2 ) 判斷當前的ThreadLocalMap是否存在, 如果存在,則呼叫map.remove,以當前ThreadLocalkey刪除對應的實體entry

  • 示例程式碼:
      /**
       * 刪除當前執行緒中儲存的ThreadLocal對應的實體entry
       * 如果此ThreadLocal變數在當前執行緒中呼叫 {@linkplain #get read}方法
       * 則會通過呼叫{@link #initialValue}進行再次初始化,
       * 除非此值value是通過當前執行緒內建呼叫 {@linkplain #set set}設定的
       * 這可能會導致在當前執行緒中多次呼叫{@code initialValue}方法
       *
       * @since 1.5
       */
       public void remove() {
          // 獲取當前執行緒物件中維護的ThreadLocalMap物件
           ThreadLocalMap m = getMap(Thread.currentThread());
          // 如果此map存在
           if (m != null)
              // 存在則呼叫map.remove
              // 以當前ThreadLocal為key刪除對應的實體entry
               m.remove(this);
       }複製程式碼

    4.

    問:對ThreadLocal的常用操作實際是對執行緒Thread中的ThreadLocalMap進行操作,核心是ThreadLocalMap這個雜湊表,你能談談ThreadLocalMap的內部底層實現嗎?

答:

  • ThreadLocalMap的底層實現是一個定製的自定義HashMap雜湊表,核心組成元素有:

    1 ) Entry[] table;:底層雜湊表 table, 必要時需要進行擴容,底層雜湊表 table.length 長度必須是2的n次方。

    2 ) int size;:實際儲存鍵值對元素個數 entries

    3 ) int threshold;:下一次擴容時的閾值,閾值 threshold = 底層雜湊表table的長度 len * 2 / 3。當size >= threshold時,遍歷table並刪除keynull的元素,如果刪除後size >= threshold*3/4時,需要對table進行擴容(詳情請檢視set(ThreadLocal<?> key, Object value)方法說明)。

  • 其中Entry[] table;雜湊表儲存的核心元素是EntryEntry包含:

    1 ) ThreadLocal<?> k;:當前儲存的ThreadLocal例項物件

    2 ) Object value;:當前 ThreadLocal 對應儲存的值value

  • 需要注意的是,此Entry繼承了弱引用 WeakReference,所以在使用ThreadLocalMap時,發現key == null,則意味著此key ThreadLocal不在被引用,需要將其從ThreadLocalMap雜湊表中移除。(弱引用相關問題解釋請檢視 問答 5)

示例程式碼:

    /**
     * ThreadLocalMap 是一個定製的自定義 hashMap 雜湊表,只適合用於維護
     * 執行緒對應ThreadLocal的值. 此類的方法沒有在ThreadLocal 類外部暴露,
     * 此類是私有的,允許在 Thread 類中以欄位的形式宣告 ,     
     * 以助於處理儲存量大,生命週期長的使用用途,
     * 此類定製的雜湊表實體鍵值對使用弱引用WeakReferences 作為key, 
     * 但是, 一旦引用不在被使用,
     * 只有當雜湊表中的空間被耗盡時,對應不再使用的鍵值對實體才會確保被 移除回收。
     */
    static class ThreadLocalMap {

        /**
         * 實體entries在此hash map中是繼承弱引用 WeakReference, 
         * 使用ThreadLocal 作為 key 鍵.  請注意,當key為null(i.e. entry.get()
         * == null) 意味著此key不再被引用,此時實體entry 會從雜湊表中刪除。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 當前 ThreadLocal 對應儲存的值value. */
            Object value;

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

        /**
         * 初始容量大小 16 -- 必須是2的n次方.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 底層雜湊表 table, 必要時需要進行擴容.
         * 底層雜湊表 table.length 長度必須是2的n次方.
         */
        private Entry[] table;

        /**
         * 實際儲存鍵值對元素個數 entries.
         */
        private int size = 0;

        /**
         * 下一次擴容時的閾值
         */
        private int threshold; // 預設為 0

        /**
         * 設定觸發擴容時的閾值 threshold
         * 閾值 threshold = 底層雜湊表table的長度 len * 2 / 3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 獲取該位置i對應的下一個位置index
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 獲取該位置i對應的上一個位置index
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

    }複製程式碼
  • ThreadLocalMap的構造方法是延遲載入的,也就是說,只有當執行緒需要儲存對應的ThreadLocal的值時,才初始化建立一次(僅初始化一次)。初始化步驟如下:

    1) 初始化底層陣列table的初始容量為 16。

    2) 獲取ThreadLocal中的threadLocalHashCode,通過threadLocalHashCode & (INITIAL_CAPACITY - 1),即ThreadLocal 的 hash 值 threadLocalHashCode % 雜湊表的長度 length 的方式計算該實體的儲存位置。

    3) 儲存當前的實體,key 為 : 當前ThreadLocal value:真正要儲存的值

    4)設定當前實際儲存元素個數 size 為 1

    5)設定閾值setThreshold(INITIAL_CAPACITY),為初始化容量 16 的 2/3。

示例程式碼:


        /**
         * 用於建立一個新的hash map包含 (firstKey, firstValue).
         * ThreadLocalMaps 構造方法是延遲載入的,所以我們只會在至少有一個
         * 實體entry存放時,才初始化建立一次(僅初始化一次)。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化 table 初始容量為 16
            table = new Entry[INITIAL_CAPACITY];
            // 計算當前entry的儲存位置
            // 儲存位置計算等價於:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 雜湊表的長度 length
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 儲存當前的實體,key 為 : 當前ThreadLocal  value:真正要儲存的值
            table[i] = new Entry(firstKey, firstValue);
            // 設定當前實際儲存元素個數 size 為 1
            size = 1;
            // 設定閾值,為初始化容量 16 的 2/3。
            setThreshold(INITIAL_CAPACITY);
        }複製程式碼
  • ThreadLocalget()操作實際是呼叫ThreadLocalMapgetEntry(ThreadLocal<?> key)方法,此方法快速適用於獲取某一存在key的實體 entry,否則,應該呼叫getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法獲取,這樣做是為了最大限制地提高直接命中的效能,該方法進行了如下操作:

    1 ) 計算要獲取的entry的儲存位置,儲存位置計算等價於:ThreadLocalhashthreadLocalHashCode % 雜湊表的長度 length

    2 ) 根據計算的儲存位置,獲取到對應的實體 Entry。判斷對應實體Entry是否存在 並且 key是否相等:

  • 存在對應實體Entry並且對應key相等,即同一ThreadLocal,返回對應的實體Entry

  • 不存在對應實體Entry 或者 key不相等,則通過呼叫getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法繼續查詢。

  • getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法操作如下:

    1 ) 獲取底層雜湊表陣列table,迴圈遍歷對應要查詢的實體Entry所關聯的位置。

    2 ) 獲取當前遍歷的entrykey ThreadLocal,比較key是否一致,一致則返回。

    3 ) 如果key不一致 並且 keynull,則證明引用已經不存在,這是因為Entry繼承的是WeakReference,這是弱引用帶來的坑。呼叫expungeStaleEntry(int staleSlot)方法刪除過期的實體Entry(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明)。

    4 ) key不一致 ,key也不為空,則遍歷下一個位置,繼續查詢。

    5 ) 遍歷完畢,仍然找不到則返回null

示例程式碼:


        /**
         * 根據key 獲取對應的實體 entry.  此方法快速適用於獲取某一存在key的
         * 實體 entry,否則,應該呼叫getEntryAfterMiss方法獲取,這樣做是為
         * 了最大限制地提高直接命中的效能
         *
         * @param  key 當前thread local 物件
         * @return the entry 對應key的 實體entry, 如果不存在,則返回null
         */
        private Entry getEntry(ThreadLocal<?> key) {
            // 計算要獲取的entry的儲存位置
            // 儲存位置計算等價於:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 雜湊表
            的長度 length
            int i = key.threadLocalHashCode & (table.length - 1);
            // 獲取到對應的實體 Entry 
            Entry e = table[i];
            // 存在對應實體並且對應key相等,即同一ThreadLocal
            if (e != null && e.get() == key)
                // 返回對應的實體Entry 
                return e;
            else
                // 不存在 或 key不一致,則通過呼叫getEntryAfterMiss繼續查詢
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * 當根據key找不到對應的實體entry 時,呼叫此方法。
         * 直接定位到對應的雜湊表位置
         *
         * @param  key 當前thread local 物件
         * @param  i 此物件在雜湊表 table中的儲存位置 index
         * @param  e the entry 實體物件
         * @return the entry 對應key的 實體entry, 如果不存在,則返回null
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 迴圈遍歷當前位置的所有實體entry
            while (e != null) {
                // 獲取當前entry 的 key ThreadLocal
                ThreadLocal<?> k = e.get();
               // 比較key是否一致,一致則返回
                if (k == key)
                    return e;
                // 找到對應的entry ,但其key 為 null,則證明引用已經不存在
                // 這是因為Entry繼承的是WeakReference,這是弱引用帶來的坑
                if (k == null)
                    // 刪除過期(stale)的entry
                    expungeStaleEntry(i);
                else
                    // key不一致 ,key也不為空,則遍歷下一個位置,繼續查詢
                    i = nextIndex(i, len);
                // 獲取下一個位置的實體 entry
                e = tab[i];
            }
            // 遍歷完畢,找不到則返回null
            return null;
        }


        /**
         * 刪除對應位置的過期實體,並刪除此位置後對應相關聯位置key = null的實體
         *
         * @param staleSlot 已知的key = null 的對應的位置索引
         * @return 對應過期實體位置索引的下一個key = null的位置
         * (所有的對應位置都會被檢查)
         */
        private int expungeStaleEntry(int staleSlot) {
            // 獲取對應的底層雜湊表 table
            Entry[] tab = table;
            // 獲取雜湊表長度
            int len = tab.length;

            // 擦除這個位置上的髒資料
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 直到我們找到 Entry e = null,才執行rehash操作
            // 就是遍歷完該位置的所有關聯位置的實體
            Entry e;
            int i;
            // 查詢該位置對應所有關聯位置的過期實體,進行擦除操作
            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;

                        // 我們必須一直遍歷直到最後
                        // 因為還可能存在多個過期的實體
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * 刪除所有過期的實體
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }複製程式碼
  • ThreadLocalset(T value)操作實際是呼叫ThreadLocalMapset(ThreadLocal<?> key, Object value)方法,該方法進行了如下操作:

    1 ) 獲取對應的底層雜湊表table,計算對應threalocal的儲存位置。

    2 ) 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal

    3 ) 獲取當前位置的threadLocal,如果key threadLocal一致,則證明找到對應的threadLocal,將新值賦值給找到的當前實體Entryvalue中,結束。

    4 ) 如果當前位置的key threadLocal不一致,並且key threadLocalnull,則呼叫replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明),替換該位置key == null 的實體為當前要設定的實體,結束。

    5 ) 如果當前位置的key threadLocal不一致,並且key threadLocal不為null,則建立新的實體,並存放至當前位置 i tab[i] = new Entry(key, value);,實際儲存鍵值對元素個數size + 1,由於弱引用帶來了這個問題,所以要呼叫cleanSomeSlots(int i, int n)方法清除無用資料(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明),才能判斷現在的size有沒有達到閥值threshhold,如果沒有要清除的資料,儲存元素個數仍然 大於 閾值 則呼叫rehash方法進行擴容(此方法不單獨解釋,請檢視示例程式碼,有詳細註釋說明)。

示例程式碼:


        /**
         * 設定對應ThreadLocal的值
         *
         * @param key 當前thread local 物件
         * @param value 要設定的值
         */
        private void set(ThreadLocal<?> key, Object value) {

            // 我們不會像get()方法那樣使用快速設定的方式,
            // 因為通常很少使用set()方法去建立新的實體
            // 相對於替換一個已經存在的實體, 在這種情況下,
            // 快速設定方案會經常失敗。

            // 獲取對應的底層雜湊表 table
            Entry[] tab = table;
            // 獲取雜湊表長度
            int len = tab.length;
            // 計算對應threalocal的儲存位置
            int i = key.threadLocalHashCode & (len-1);

            // 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 獲取當前位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果key threadLocal一致,則證明找到對應的threadLocal
                if (k == key) {
                    // 賦予新值
                    e.value = value;
                    // 結束
                    return;
                }
                // 如果當前位置的key threadLocal為null
                if (k == null) {
                    // 替換該位置key == null 的實體為當前要設定的實體
                    replaceStaleEntry(key, value, i);
                    // 結束
                    return;
                }
            }
            // 當前位置的k != key  && k != null
            // 建立新的實體,並存放至當前位置i
            tab[i] = new Entry(key, value);
            // 實際儲存鍵值對元素個數 + 1
            int sz = ++size;
            // 由於弱引用帶來了這個問題,所以先要清除無用資料,才能判斷現在的size有沒有達到閥值threshhold
            // 如果沒有要清除的資料,儲存元素個數仍然 大於 閾值 則擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 擴容
                rehash();
        }

        /**
         * 當執行set操作時,獲取對應的key threadLocal,並替換過期的實體
         * 將這個value值儲存在對應key threadLocal的實體中,無論是否已經存在體
         * 對應的key threadLocal
         *
         * 有一個副作用, 此方法會刪除該位置下和該位置nextIndex對應的所有過期的實體
         *
         * @param  key 當前thread local 物件
         * @param  value 當前thread local 物件對應儲存的值
         * @param  staleSlot 第一次找到此過期的實體對應的位置索引index
         *         .
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            // 獲取對應的底層雜湊表 table
            Entry[] tab = table;
            // 獲取雜湊表長度
            int len = tab.length;
            Entry e;

            // 往前找,找到table中第一個過期的實體的下標
            // 清理整個table是為了避免因為垃圾回收帶來的連續增長雜湊的危險
            // 也就是說,雜湊表沒有清理乾淨,當GC到來的時候,後果很嚴重

            // 記錄要清除的位置的起始首位置
            int slotToExpunge = staleSlot;
            // 從該位置開始,往前遍歷查詢第一個過期的實體的下標
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 找到key一致的ThreadLocal或找到一個key為 null的
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // 如果我們找到了key,那麼我們就需要把它跟新的過期資料交換來保持雜湊表的順序
                // 那麼剩下的過期Entry呢,就可以交給expungeStaleEntry方法來擦除掉
                // 將新設定的實體放置在此過期的實體的位置上
                if (k == key) {
                    // 替換,將要設定的值放在此過期的實體中
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果存在,則開始清除之前過期的實體
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 在這裡開始清除過期資料
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // / 如果我們沒有在往後查詢中找沒有找到過期的實體,
                // 那麼slotToExpunge就是第一個過期Entry的下標了
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 最後key仍沒有找到,則將要設定的新實體放置
            // 在原過期的實體對應的位置上。
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果該位置對應的其他關聯位置存在過期實體,則清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }


        /**
         * 啟發式的掃描查詢一些過期的實體並清除,
         * 此方法會再新增新實體的時候被呼叫, 
         * 或者過期的元素被清除時也會被呼叫.
         * 如果實在沒有過期資料,那麼這個演算法的時間複雜度就是O(log n)
         * 如果有過期資料,那麼這個演算法的時間複雜度就是O(n)
         * 
         * @param i 一個確定不是過期的實體的位置,從這個位置i開始掃描
         *
         * @param n 掃描控制: 有{@code log2(n)} 單元會被掃描,
         * 除非找到了過期的實體, 在這種情況下
         * 有{@code log2(table.length)-1} 的格外單元會被掃描.
         * 當呼叫插入時, 這個引數的值是儲存實體的個數,
         * 但如果呼叫 replaceStaleEntry方法, 這個值是雜湊表table的長度
         * (注意: 所有的這些都可能或多或少的影響n的權重
         * 但是這個版本簡單,快速,而且似乎執行效率還可以)
         *
         * @return true 返回true,如果有任何過期的實體被刪除。
         */
        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;
        }


        /**
         * 雜湊表擴容方法
         * 首先掃描整個雜湊表table,刪除過期的實體
         * 縮小雜湊表table大小 或 擴大雜湊表table大小,擴大的容量是加倍.
         */
        private void rehash() {
            // 刪除所有過期的實體
            expungeStaleEntries();

            // 使用較低的閾值threshold加倍以避免滯後
            // 儲存實體個數 大於等於 閾值的3/4則擴容
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * 擴容方法,以2倍的大小進行擴容
         * 擴容的思想跟HashMap很相似,都是把容量擴大兩倍
         * 不同之處還是因為WeakReference帶來的
         */
        private void resize() {
            // 記錄舊的雜湊表
            Entry[] oldTab = table;
            // 記錄舊的雜湊表長度
            int oldLen = oldTab.length;
            // 新的雜湊表長度為舊的雜湊表長度的2倍
            int newLen = oldLen * 2;
            // 建立新的雜湊表
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 逐一遍歷舊的雜湊表table的每個實體,重新分配至新的雜湊表中
            for (int j = 0; j < oldLen; ++j) {
                // 獲取對應位置的實體
                Entry e = oldTab[j];
                // 如果實體不會null
                if (e != null) {
                    // 獲取實體對應的ThreadLocal
                    ThreadLocal<?> k = e.get(); 
                    // 如果該ThreadLocal 為 null
                    if (k == null) {
                        // 則對應的值也要清除
                        // 就算是擴容,也不能忘了為擦除過期資料做準備
                        e.value = null; // Help the GC
                    } else {
                        // 如果不是過期實體,則根據新的長度重新計算儲存位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                       // 將該實體儲存在對應ThreadLocal的最後一個位置
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            // 重新分配位置完畢,則重新計算閾值Threshold
            setThreshold(newLen);
            // 記錄實際儲存元素個數
            size = count;
            // 將新的雜湊表賦值至底層table
            table = newTab;
        }複製程式碼
  • ThreadLocalremove()操作實際是呼叫ThreadLocalMapremove(ThreadLocal<?> key)方法,該方法進行了如下操作:

    1 ) 獲取對應的底層雜湊表 table,計算對應threalocal的儲存位置。

    2 ) 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal

    3 ) 獲取當前位置的threadLocal,如果key threadLocal一致,則證明找到對應的threadLocal,執行刪除操作,刪除此位置的實體,結束。

示例程式碼:


        /**
         * 移除對應ThreadLocal的實體
         */
        private void remove(ThreadLocal<?> key) {
            // 獲取對應的底層雜湊表 table
            Entry[] tab = table;
            // 獲取雜湊表長度
            int len = tab.length;
            // 計算對應threalocal的儲存位置
            int i = key.threadLocalHashCode & (len-1);
            // 迴圈遍歷table對應該位置的實體,查詢對應的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 如果key threadLocal一致,則證明找到對應的threadLocal
                if (e.get() == key) {
                    // 執行清除操作
                    e.clear();
                    // 清除此位置的實體
                    expungeStaleEntry(i);
                    // 結束
                    return;
                }
            }
        }複製程式碼

5.

問:ThreadLocalMap中的儲存實體Entry使用ThreadLocal作為key,但這個Entry是繼承弱引用WeakReference的,為什麼要這樣設計,使用了弱引用WeakReference會造成記憶體洩露問題嗎?

答:

  • 首先,回答這個問題之前,我需要解釋一下什麼是強引用,什麼是弱引用。

我們在正常情況下,普遍使用的是強引用:

A a = new A();

B b = new B();複製程式碼

a = null;b = null;時,一段時間後,JAVA垃圾回收機制GC會將 a 和 b 對應所分配的記憶體空間給回收。

但考慮這樣一種情況:

C c = new C(b);
b = null;複製程式碼

當 b 被設定成null時,那麼是否意味這一段時間後GC工作可以回收 b 所分配的記憶體空間呢?答案是否定的,因為即使 b 被設定成null,但 c 仍然持有對 b 的引用,而且還是強引用,所以GC不會回收 b 原先所分配的空間,既不能回收,又不能使用,這就造成了 記憶體洩露。

那麼如何處理呢?

可以通過c = null;,也可以使用弱引用WeakReference w = new WeakReference(b);。因為使用了弱引用WeakReference,GC是可以回收 b 原先所分配的空間的。

上述解釋主要參考自:對ThreadLocal實現原理的一點思考

  • 回到ThreadLocal的層面上,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現keynullEntry,就沒有辦法訪問這些keynullEntryvalue,如果當前執行緒再遲遲不結束的話,這些keynullEntryvalue就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠無法回收,造成記憶體洩漏。

其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocalget(),set(),remove()的時候都會清除執行緒ThreadLocalMap裡所有keynullvalue

但是這些被動的預防措施並不能保證不會記憶體洩漏:

  • 使用staticThreadLocal,延長了ThreadLocal的生命週期,可能導致的記憶體洩漏(參考ThreadLocal 記憶體洩露的例項分析)。

  • 分配使用了ThreadLocal又不再呼叫get(),set(),remove()方法,那麼就會導致記憶體洩漏。

從表面上看記憶體洩漏的根源在於使用了弱引用。網上的文章大多著重分析ThreadLocal使用了弱引用會導致記憶體洩漏,但是另一個問題也同樣值得思考:為什麼使用弱引用而不是強引用?

我們先來看看官方文件的說法:

To help deal with very large and long-lived usages, 
the hash table entries use WeakReferences for keys.複製程式碼

為了應對非常大和長時間的用途,雜湊表使用弱引用的 key

下面我們分兩種情況討論:

  • key 使用強引用:引用的ThreadLocal的物件被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry記憶體洩漏。

  • key 使用弱引用:引用的ThreadLocal的物件被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap呼叫get(),set(),remove()的時候會被清除。

  • 比較兩種情況,我們可以發現:由於ThreadLocalMap的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致記憶體洩漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會記憶體洩漏,對應的value在下一次ThreadLocalMap呼叫get(),set(),remove()的時候會被清除。

因此,ThreadLocal記憶體洩漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key就會導致記憶體洩漏,而不是因為弱引用。

綜合上面的分析,我們可以理解ThreadLocal記憶體洩漏的前因後果,那麼怎麼避免記憶體洩漏呢?

每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料。

在使用執行緒池的情況下,沒有及時清理ThreadLocal,不僅是記憶體洩漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。

上述解釋主要參考自:深入分析 ThreadLocal 記憶體洩漏問題

6.

問:ThreadLocalsynchronized的區別?

答:ThreadLocalsynchronized關鍵字都用於處理多執行緒併發訪問變數的問題,只是二者處理問題的角度和思路不同。

  1. ThreadLocal是一個Java類,通過對當前執行緒中的區域性變數的操作來解決不同執行緒的變數訪問的衝突問題。所以,ThreadLocal提供了執行緒安全的共享物件機制,每個執行緒都擁有其副本。

  2. Java中的synchronized是一個保留字,它依靠JVM的鎖機制來實現臨界區的函式或者變數的訪問中的原子性。在同步機制中,通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。此時,被用作“鎖機制”的變數時多個執行緒共享的。

  • 同步機制(synchronized關鍵字)採用了以“時間換空間”的方式,提供一份變數,讓不同的執行緒排隊訪問。而ThreadLocal採用了“以空間換時間”的方式,為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。

7.

問:ThreadLocal在現時有什麼應用場景?

答:總的來說ThreadLocal主要是解決2種型別的問題:

  • 解決併發問題:使用ThreadLocal代替synchronized來保證執行緒安全。同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變數,讓不同的執行緒排隊訪問,而後者為每一個執行緒都提供了一份變數,因此可以同時訪問而互不影響。

  • 解決資料儲存問題:ThreadLocal為變數在每個執行緒中都建立了一個副本,所以每個執行緒可以訪問自己內部的副本變數,不同執行緒之間不會互相干擾。如一個Parameter物件的資料需要在多個模組中使用,如果採用引數傳遞的方式,顯然會增加模組之間的耦合性。此時我們可以使用ThreadLocal解決。

應用場景:

Spring使用ThreadLocal解決執行緒安全問題

  • 我們知道在一般情況下,只有無狀態的Bean才可以在多執行緒環境下共享,在Spring中,絕大部分Bean都可以宣告為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolderTransactionSynchronizationManagerLocaleContextHolder等)中非執行緒安全狀態採用ThreadLocal進行處理,讓它們也成為執行緒安全的狀態,因為有狀態的Bean就可以在多執行緒中共享了。

  • 一般的Web應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒ThreadLocal是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的併發性。

示例程式碼:

public abstract class RequestContextHolder  {
····

    private static final boolean jsfPresent =
            ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());

    private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");

    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
            new NamedInheritableThreadLocal<RequestAttributes>("Request context");

·····
}複製程式碼

總結

  1. ThreadLocal提供執行緒內部的區域性變數,在本執行緒內隨時隨地可取,隔離其他執行緒。

  2. ThreadLocal的設計是:每個Thread維護一個ThreadLocalMap雜湊表,這個雜湊表的keyThreadLocal例項本身,value才是真正要儲存的值Object

  3. ThreadLocal的常用操作實際是對執行緒Thread中的ThreadLocalMap進行操作。

  4. ThreadLocalMap的底層實現是一個定製的自定義HashMap雜湊表,ThreadLocalMap的閾值threshold = 底層雜湊表table的長度 len * 2 / 3,當實際儲存元素個數size 大於或等於 閾值threshold3/4size >= threshold*3/4,則對底層雜湊表陣列table進行擴容操作。

  5. ThreadLocalMap中的雜湊表Entry[] table儲存的核心元素是Entry,儲存的keyThreadLocal例項物件,valueThreadLocal 對應儲存的值value。需要注意的是,此Entry繼承了弱引用 WeakReference,所以在使用ThreadLocalMap時,發現key == null,則意味著此key ThreadLocal不在被引用,需要將其從ThreadLocalMap雜湊表中移除。

  6. ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那麼系統 GC 的時候,這個ThreadLocal勢必會被回收。所以,在ThreadLocalget(),set(),remove()的時候都會清除執行緒ThreadLocalMap裡所有keynullvalue。如果我們不主動呼叫上述操作,則會導致記憶體洩露。

  7. 為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal後都要呼叫remove()來清理無用的Entry。這在操作在使用執行緒池時尤為重要。

  8. ThreadLocalsynchronized的區別:同步機制(synchronized關鍵字)採用了以“時間換空間”的方式,提供一份變數,讓不同的執行緒排隊訪問。而ThreadLocal採用了“以空間換時間”的方式,為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響。

  9. ThreadLocal主要是解決2種型別的問題:A. 解決併發問題:使用ThreadLocal代替同步機制解決併發問題。B. 解決資料儲存問題:如一個Parameter物件的資料需要在多個模組中使用,如果採用引數傳遞的方式,顯然會增加模組之間的耦合性。此時我們可以使用ThreadLocal解決。

參考文章

深入淺出ThreadLocal
ThreadLocal和synchronized的區別?
深入剖析ThreadLocal
ThreadLocal內部機制
聊一聊Spring中的執行緒安全性
對ThreadLocal實現原理的一點思考
深入分析 ThreadLocal 記憶體洩漏問題
學習Spring必學的Java基礎知識(6)----ThreadLocal
ThreadLocal設計模式
ThreadLocal案例分析
Spring單例模式與執行緒安全ThreadLocal

相關文章