JAVA併發-自問自答學ThreadLocal

前言

ThreadLocal不少同窗都搞不懂是什麼東西,能夠用來幹嗎。但面試時卻又常常問到,因此此次我和你們一塊兒學習ThreadLocal這個類。java

下面我就以面試問答的形式學習咱們的——ThreadLocal類(源碼分析基於JDK8)程序員

本文同步發佈於簡書 :www.jianshu.com/p/807686414…面試

問答內容

1.

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

答:設計模式

  • 從JAVA官方對ThreadLocal類的說明定義(定義在示例代碼中):ThreadLocal類用來提供線程內部的局部變量。這種變量在多線程環境下訪問(經過getset方法訪問)時能保證各個線程的變量相對獨立於其餘線程內的變量。ThreadLocal實例一般來講都是private static類型的,用於關聯線程和線程上下文。數組

  • 咱們能夠得知ThreadLocal的做用是:ThreadLocal的做用是提供線程內的局部變量,不一樣的線程之間不會相互干擾,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或組件之間一些公共變量的傳遞的複雜度。安全

  • 上述能夠概述爲:ThreadLocal提供線程內部的局部變量,在本線程內隨時隨地可取,隔離其餘線程。bash

示例代碼:多線程

/**
 * 該類提供了線程局部 (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

相關文章
相關標籤/搜索