ThreadLocal

下面我就以面試問答的形式學(xué)習(xí)我們的——ThreadLocal類(源碼分析基于JDK8)

問答內(nèi)容

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

答:從JAVA官方對ThreadLocal類的說明定義(定義在示例代碼中):ThreadLocal類用來提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問(通過get和set方法訪問)時能保證各個線程的變量相對獨立于其他線程內(nèi)的變量。ThreadLocal實例通常來說都是private static類型的,用于關(guān)聯(lián)線程和線程上下文。
我們可以得知ThreadLocal的作用是:ThreadLocal的作用是提供線程內(nèi)的局部變量,不同的線程之間不會相互干擾,這種變量在線程的生命周期內(nèi)起作用,減少同一個線程內(nèi)多個函數(shù)或組件之間一些公共變量的傳遞的復(fù)雜度。

上述可以概述為:ThreadLocal提供線程內(nèi)部的局部變量,在本線程內(nèi)隨時隨地可取,隔離其他線程。

2.ThreadLocal實現(xiàn)原理是什么,它是怎么樣做到局部變量不同的線程之間不會相互干擾的?

JDK8 ThreadLocal的設(shè)計是:每個Thread維護一個ThreadLocalMap哈希表,這個哈希表的key是ThreadLocal實例本身,value才是真正要存儲的值Object。
這樣設(shè)計有如下幾點優(yōu)勢:

  • 1、這樣設(shè)計之后每個Map存儲的Entry數(shù)量就會變小,因為之前的存儲數(shù)量由Thread的數(shù)量決定,現(xiàn)在是由ThreadLocal的數(shù)量決定。
  • 2、當(dāng)Thread銷毀之后,對應(yīng)的ThreadLocalMap也會隨之銷毀,能減少內(nèi)存的使用。

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

調(diào)用get()操作獲取ThreadLocal中對應(yīng)當(dāng)前線程存儲的值時,進行了如下操作:

  • 1、獲取當(dāng)前線程Thread對象,進而獲取此線程對象中維護的ThreadLocalMap對象。
  • 2、判斷當(dāng)前的ThreadLocalMap是否存在:
    如果存在,則以當(dāng)前的ThreadLocal 為 key,調(diào)用ThreadLocalMap中的getEntry方法獲取對應(yīng)的存儲實體 e。找到對應(yīng)的存儲實體 e,獲取存儲實體 e 對應(yīng)的 value值,即為我們想要的當(dāng)前線程對應(yīng)此ThreadLocal的值,返回結(jié)果值。
    如果不存在,則證明此線程沒有維護的ThreadLocalMap對象,調(diào)用setInitialValue方法進行初始化。返回setInitialValue初始化的值。

setInitialValue方法的操作如下:

  • 1、調(diào)用initialValue獲取初始化的值。
  • 2、獲取當(dāng)前線程Thread對象,進而獲取此線程對象中維護的ThreadLocalMap對象。
  • 3、判斷當(dāng)前的ThreadLocalMap是否存在:

如果存在,則調(diào)用map.set設(shè)置此實體entry。
如果不存在,則調(diào)用createMap進行ThreadLocalMap對象的初始化,并將此實體entry作為第一個值存放至ThreadLocalMap中。

/**
     * 返回當(dāng)前線程對應(yīng)的ThreadLocal的初始值
     * 此方法的第一次調(diào)用發(fā)生在,當(dāng)線程通過{@link #get}方法訪問此線程的ThreadLocal值時
     * 除非線程先調(diào)用了 {@link #set}方法,在這種情況下,
     * {@code initialValue} 才不會被這個線程調(diào)用。
     * 通常情況下,每個線程最多調(diào)用一次這個方法,
     * 但也可能再次調(diào)用,發(fā)生在調(diào)用{@link #remove}方法后,
     * 緊接著調(diào)用{@link #get}方法。
     *
     * <p>這個方法僅僅簡單的返回null {@code null};
     * 如果程序員想ThreadLocal線程局部變量有一個除null以外的初始值,
     * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
     * 通常, 可以通過匿名內(nèi)部類的方式實現(xiàn)
     *
     * @return 當(dāng)前ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }

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

    /**
     * 返回當(dāng)前線程中保存ThreadLocal的值
     * 如果當(dāng)前線程沒有此ThreadLocal變量,
     * 則它會通過調(diào)用{@link #initialValue} 方法進行初始化值
     *
     * @return 返回當(dāng)前線程對應(yīng)此ThreadLocal的值
     */
    public T get() {
        // 獲取當(dāng)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以當(dāng)前的ThreadLocal 為 key,調(diào)用getEntry獲取對應(yīng)的存儲實體e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 找到對應(yīng)的存儲實體 e 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 獲取存儲實體 e 對應(yīng)的 value值
                // 即為我們想要的當(dāng)前線程對應(yīng)此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map不存在,則證明此線程沒有維護的ThreadLocalMap對象
        // 調(diào)用setInitialValue進行初始化
        return setInitialValue();
    }

    /**
     * set的變樣實現(xiàn),用于初始化值initialValue,
     * 用于代替防止用戶重寫set()方法
     *
     * @return the initial value 初始化后的值
     */
    private T setInitialValue() {
        // 調(diào)用initialValue獲取初始化的值
        T value = initialValue();
        // 獲取當(dāng)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則調(diào)用map.set設(shè)置此實體entry
            map.set(this, value);
        else
            // 1)當(dāng)前線程Thread 不存在ThreadLocalMap對象
            // 2)則調(diào)用createMap進行ThreadLocalMap對象的初始化
            // 3)并將此實體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
        // 返回設(shè)置的值value
        return value;
    }

    /**
     * 獲取當(dāng)前線程Thread對應(yīng)維護的ThreadLocalMap 
     * 
     * @param  t the current thread 當(dāng)前線程
     * @return the map 對應(yīng)維護的ThreadLocalMap 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

調(diào)用set(T value)操作設(shè)置ThreadLocal中對應(yīng)當(dāng)前線程要存儲的值時,進行了如下操作:

  • 1、獲取當(dāng)前線程Thread對象,進而獲取此線程對象中維護的ThreadLocalMap對象。
  • 2、判斷當(dāng)前的ThreadLocalMap是否存在:

如果存在,則調(diào)用map.set設(shè)置此實體entry。
如果不存在,則調(diào)用createMap進行ThreadLocalMap對象的初始化,并將此實體entry作為第一個值存放至ThreadLocalMap中。

/**
     * 設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal的值
     * 大多數(shù)子類都不需要重寫此方法,
     * 只需要重寫 {@link #initialValue}方法代替設(shè)置當(dāng)前線程對應(yīng)的ThreadLocal的值
     *
     * @param value 將要保存在當(dāng)前線程對應(yīng)的ThreadLocal的值
     *  
     */
    public void set(T value) {
        // 獲取當(dāng)前線程對象
        Thread t = Thread.currentThread();
        // 獲取此線程對象中維護的ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null)
            // 存在則調(diào)用map.set設(shè)置此實體entry
            map.set(this, value);
        else
            // 1)當(dāng)前線程Thread 不存在ThreadLocalMap對象
            // 2)則調(diào)用createMap進行ThreadLocalMap對象的初始化
            // 3)并將此實體entry作為第一個值存放至ThreadLocalMap中
            createMap(t, value);
    }

    /**
     * 為當(dāng)前線程Thread 創(chuàng)建對應(yīng)維護的ThreadLocalMap. 
     *
     * @param t the current thread 當(dāng)前線程
     * @param firstValue 第一個要存放的ThreadLocal變量值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

調(diào)用remove()操作刪除ThreadLocal中對應(yīng)當(dāng)前線程已存儲的值時,進行了如下操作:

  • 1、獲取當(dāng)前線程Thread對象,進而獲取此線程對象中維護的ThreadLocalMap對象。
  • 2、判斷當(dāng)前的ThreadLocalMap是否存在, 如果存在,則調(diào)用map.remove,以當(dāng)前ThreadLocal為key刪除對應(yīng)的實體entry。
/**
   * 刪除當(dāng)前線程中保存的ThreadLocal對應(yīng)的實體entry
   * 如果此ThreadLocal變量在當(dāng)前線程中調(diào)用 {@linkplain #get read}方法
   * 則會通過調(diào)用{@link #initialValue}進行再次初始化,
   * 除非此值value是通過當(dāng)前線程內(nèi)置調(diào)用 {@linkplain #set set}設(shè)置的
   * 這可能會導(dǎo)致在當(dāng)前線程中多次調(diào)用{@code initialValue}方法
   *
   * @since 1.5
   */
   public void remove() {
      // 獲取當(dāng)前線程對象中維護的ThreadLocalMap對象
       ThreadLocalMap m = getMap(Thread.currentThread());
      // 如果此map存在
       if (m != null)
          // 存在則調(diào)用map.remove
          // 以當(dāng)前ThreadLocal為key刪除對應(yīng)的實體entry
           m.remove(this);
   }

4、對ThreadLocal的常用操作實際是對線程Thread中的ThreadLocalMap進行操作,核心是ThreadLocalMap這個哈希表,你能談?wù)凾hreadLocalMap的內(nèi)部底層實現(xiàn)嗎?

ThreadLocalMap的底層實現(xiàn)是一個定制的自定義HashMap哈希表,核心組成元素有:

  • 1、Entry[] table;:底層哈希表 table, 必要時需要進行擴容,底層哈希表 table.length 長度必須是2的n次方。
  • 2、int size;:實際存儲鍵值對元素個數(shù) entries
  • 3、int threshold;:下一次擴容時的閾值,閾值 threshold = 底層哈希表table的長度 len * 2 / 3。當(dāng)size >= threshold時,遍歷table并刪除key為null的元素,如果刪除后size >= threshold*3/4時,需要對table進行擴容(詳情請查看set(ThreadLocal<?> key, Object value)方法說明)。

其中Entry[] table;哈希表存儲的核心元素是Entry,Entry包含:

  • 1、ThreadLocal<?> k;:當(dāng)前存儲的ThreadLocal實例對象
  • 2、Object value;:當(dāng)前 ThreadLocal 對應(yīng)儲存的值value

需要注意的是,此Entry繼承了弱引用 WeakReference,所以在使用ThreadLocalMap時,發(fā)現(xiàn)key == null,則意味著此key ThreadLocal不在被引用,需要將其從ThreadLocalMap哈希表中移除。

示例代碼:

/**
     * ThreadLocalMap 是一個定制的自定義 hashMap 哈希表,只適合用于維護
     * 線程對應(yīng)ThreadLocal的值. 此類的方法沒有在ThreadLocal 類外部暴露,
     * 此類是私有的,允許在 Thread 類中以字段的形式聲明 ,     
     * 以助于處理存儲量大,生命周期長的使用用途,
     * 此類定制的哈希表實體鍵值對使用弱引用WeakReferences 作為key, 
     * 但是, 一旦引用不在被使用,
     * 只有當(dāng)哈希表中的空間被耗盡時,對應(yīng)不再使用的鍵值對實體才會確保被 移除回收。
     */
    static class ThreadLocalMap {

        /**
         * 實體entries在此hash map中是繼承弱引用 WeakReference, 
         * 使用ThreadLocal 作為 key 鍵.  請注意,當(dāng)key為null(i.e. entry.get()
         * == null) 意味著此key不再被引用,此時實體entry 會從哈希表中刪除。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 當(dāng)前 ThreadLocal 對應(yīng)儲存的值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;

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

        /**
         * 下一次擴容時的閾值
         */
        private int threshold; // 默認(rèn)為 0

        /**
         * 設(shè)置觸發(fā)擴容時的閾值 threshold
         * 閾值 threshold = 底層哈希表table的長度 len * 2 / 3
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

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

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

    }

ThreadLocalMap的構(gòu)造方法是延遲加載的,也就是說,只有當(dāng)線程需要存儲對應(yīng)的ThreadLocal的值時,才初始化創(chuàng)建一次(僅初始化一次)。

初始化步驟如下:

  • 1初始化底層數(shù)組table的初始容量為 16。
  • 2獲取ThreadLocal中的threadLocalHashCode,通過threadLocalHashCode & (INITIAL_CAPACITY - 1),即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的長度 length 的方式計算該實體的存儲位置。
  • 3存儲當(dāng)前的實體,key 為 : 當(dāng)前ThreadLocal value:真正要存儲的值
  • 4設(shè)置當(dāng)前實際存儲元素個數(shù) size 為 1
  • 5設(shè)置閾值setThreshold(INITIAL_CAPACITY),為初始化容量 16 的 2/3。

示例代碼:

/**
         * 用于創(chuàng)建一個新的hash map包含 (firstKey, firstValue).
         * ThreadLocalMaps 構(gòu)造方法是延遲加載的,所以我們只會在至少有一個
         * 實體entry存放時,才初始化創(chuàng)建一次(僅初始化一次)。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化 table 初始容量為 16
            table = new Entry[INITIAL_CAPACITY];
            // 計算當(dāng)前entry的存儲位置
            // 存儲位置計算等價于:
            // ThreadLocal 的 hash 值 threadLocalHashCode  % 哈希表的長度 length
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 存儲當(dāng)前的實體,key 為 : 當(dāng)前ThreadLocal  value:真正要存儲的值
            table[i] = new Entry(firstKey, firstValue);
            // 設(shè)置當(dāng)前實際存儲元素個數(shù) size 為 1
            size = 1;
            // 設(shè)置閾值,為初始化容量 16 的 2/3。
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocal的get()操作實際是調(diào)用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,此方法快速適用于獲取某一存在key的實體 entry,否則,應(yīng)該調(diào)用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法獲取,這樣做是為了最大限制地提高直接命中的性能,該方法進行了如下操作:

  • 1、計算要獲取的entry的存儲位置,存儲位置計算等價于:ThreadLocal的 hash 值 threadLocalHashCode % 哈希表的長度 length。
  • 2、根據(jù)計算的存儲位置,獲取到對應(yīng)的實體 Entry。判斷對應(yīng)實體Entry是否存在 并且 key是否相等:
    存在對應(yīng)實體Entry并且對應(yīng)key相等,即同一ThreadLocal,返回對應(yīng)的實體Entry。
    不存在對應(yīng)實體Entry 或者 key不相等,則通過調(diào)用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法繼續(xù)查找。

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

  • 1.獲取底層哈希表數(shù)組table,循環(huán)遍歷對應(yīng)要查找的實體Entry所關(guān)聯(lián)的位置。
  • 2.獲取當(dāng)前遍歷的entry 的 key ThreadLocal,比較key是否一致,一致則返回。
  • 3.如果key不一致 并且 key 為 null,則證明引用已經(jīng)不存在,這是因為Entry繼承的是WeakReference,這是弱引用帶來的坑。調(diào)用expungeStaleEntry(int staleSlot)方法刪除過期的實體Entry(此方法不單獨解釋,請查看示例代碼,有詳細(xì)注釋說明)。
    1. key不一致 ,key也不為空,則遍歷下一個位置,繼續(xù)查找。
  • 5.遍歷完畢,仍然找不到則返回null。

示例代碼:

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

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


        /**
         * 刪除對應(yīng)位置的過期實體,并刪除此位置后對應(yīng)相關(guān)聯(lián)位置key = null的實體
         *
         * @param staleSlot 已知的key = null 的對應(yīng)的位置索引
         * @return 對應(yīng)過期實體位置索引的下一個key = null的位置
         * (所有的對應(yīng)位置都會被檢查)
         */
        private int expungeStaleEntry(int staleSlot) {
            // 獲取對應(yīng)的底層哈希表 table
            Entry[] tab = table;
            // 獲取哈希表長度
            int len = tab.length;

            // 擦除這個位置上的臟數(shù)據(jù)
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 直到我們找到 Entry e = null,才執(zhí)行rehash操作
            // 就是遍歷完該位置的所有關(guān)聯(lián)位置的實體
            Entry e;
            int i;
            // 查找該位置對應(yīng)所有關(guān)聯(lián)位置的過期實體,進行擦除操作
            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);
            }
        }

ThreadLocal的set(T value)操作實際是調(diào)用ThreadLocalMap的set(ThreadLocal<?> key, Object value)方法,該方法進行了如下操作:

  • 1.獲取對應(yīng)的底層哈希表table,計算對應(yīng)threalocal的存儲位置。
  • 2.循環(huán)遍歷table對應(yīng)該位置的實體,查找對應(yīng)的threadLocal。
  • 3.獲取當(dāng)前位置的threadLocal,如果key threadLocal一致,則證明找到對應(yīng)的threadLocal,將新值賦值給找到的當(dāng)前實體Entry的value中,結(jié)束。
  • 4.如果當(dāng)前位置的key threadLocal不一致,并且key threadLocal為null,則調(diào)用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方法不單獨解釋,請查看示例代碼,有詳細(xì)注釋說明),替換該位置key == null 的實體為當(dāng)前要設(shè)置的實體,結(jié)束。
  • 5.如果當(dāng)前位置的key threadLocal不一致,并且key threadLocal不為null,則創(chuàng)建新的實體,并存放至當(dāng)前位置 i tab[i] = new Entry(key, value);,實際存儲鍵值對元素個數(shù)size + 1,由于弱引用帶來了這個問題,所以要調(diào)用cleanSomeSlots(int i, int n)方法清除無用數(shù)據(jù)(此方法不單獨解釋,請查看示例代碼,有詳細(xì)注釋說明),才能判斷現(xiàn)在的size有沒有達(dá)到閥值threshhold,如果沒有要清除的數(shù)據(jù),存儲元素個數(shù)仍然 大于 閾值 則調(diào)用rehash方法進行擴容(此方法不單獨解釋,請查看示例代碼,有詳細(xì)注釋說明)。

示例代碼:

/**
         * 設(shè)置對應(yīng)ThreadLocal的值
         *
         * @param key 當(dāng)前thread local 對象
         * @param value 要設(shè)置的值
         */
        private void set(ThreadLocal<?> key, Object value) {

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

            // 獲取對應(yīng)的底層哈希表 table
            Entry[] tab = table;
            // 獲取哈希表長度
            int len = tab.length;
            // 計算對應(yīng)threalocal的存儲位置
            int i = key.threadLocalHashCode & (len-1);
            
            // 循環(huán)遍歷table對應(yīng)該位置的實體,查找對應(yīng)的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 獲取當(dāng)前位置的ThreadLocal
                ThreadLocal<?> k = e.get();
                // 如果key threadLocal一致,則證明找到對應(yīng)的threadLocal
                if (k == key) {
                    // 賦予新值
                    e.value = value;
                    // 結(jié)束
                    return;
                }
                // 如果當(dāng)前位置的key threadLocal為null
                if (k == null) {
                    // 替換該位置key == null 的實體為當(dāng)前要設(shè)置的實體
                    replaceStaleEntry(key, value, i);
                    // 結(jié)束
                    return;
                }
            }
            // 當(dāng)前位置的k != key  && k != null
            // 創(chuàng)建新的實體,并存放至當(dāng)前位置i
            tab[i] = new Entry(key, value);
            // 實際存儲鍵值對元素個數(shù) + 1
            int sz = ++size;
            // 由于弱引用帶來了這個問題,所以先要清除無用數(shù)據(jù),才能判斷現(xiàn)在的size有沒有達(dá)到閥值threshhold
            // 如果沒有要清除的數(shù)據(jù),存儲元素個數(shù)仍然 大于 閾值 則擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 擴容
                rehash();
        }

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

            // 往前找,找到table中第一個過期的實體的下標(biāo)
            // 清理整個table是為了避免因為垃圾回收帶來的連續(xù)增長哈希的危險
            // 也就是說,哈希表沒有清理干凈,當(dāng)GC到來的時候,后果很嚴(yán)重

            // 記錄要清除的位置的起始首位置
            int slotToExpunge = staleSlot;
            // 從該位置開始,往前遍歷查找第一個過期的實體的下標(biāo)
            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,那么我們就需要把它跟新的過期數(shù)據(jù)交換來保持哈希表的順序
                // 那么剩下的過期Entry呢,就可以交給expungeStaleEntry方法來擦除掉
                // 將新設(shè)置的實體放置在此過期的實體的位置上
                if (k == key) {
                    // 替換,將要設(shè)置的值放在此過期的實體中
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // 如果存在,則開始清除之前過期的實體
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 在這里開始清除過期數(shù)據(jù)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

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

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

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


        /**
         * 啟發(fā)式的掃描查找一些過期的實體并清除,
         * 此方法會再添加新實體的時候被調(diào)用, 
         * 或者過期的元素被清除時也會被調(diào)用.
         * 如果實在沒有過期數(shù)據(jù),那么這個算法的時間復(fù)雜度就是O(log n)
         * 如果有過期數(shù)據(jù),那么這個算法的時間復(fù)雜度就是O(n)
         * 
         * @param i 一個確定不是過期的實體的位置,從這個位置i開始掃描
         *
         * @param n 掃描控制: 有{@code log2(n)} 單元會被掃描,
         * 除非找到了過期的實體, 在這種情況下
         * 有{@code log2(table.length)-1} 的格外單元會被掃描.
         * 當(dāng)調(diào)用插入時, 這個參數(shù)的值是存儲實體的個數(shù),
         * 但如果調(diào)用 replaceStaleEntry方法, 這個值是哈希表table的長度
         * (注意: 所有的這些都可能或多或少的影響n的權(quán)重
         * 但是這個版本簡單,快速,而且似乎執(zhí)行效率還可以)
         *
         * @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加倍以避免滯后
            // 存儲實體個數(shù) 大于等于 閾值的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;
            // 創(chuàng)建新的哈希表
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 逐一遍歷舊的哈希表table的每個實體,重新分配至新的哈希表中
            for (int j = 0; j < oldLen; ++j) {
                // 獲取對應(yīng)位置的實體
                Entry e = oldTab[j];
                // 如果實體不會null
                if (e != null) {
                    // 獲取實體對應(yīng)的ThreadLocal
                    ThreadLocal<?> k = e.get(); 
                    // 如果該ThreadLocal 為 null
                    if (k == null) {
                        // 則對應(yīng)的值也要清除
                        // 就算是擴容,也不能忘了為擦除過期數(shù)據(jù)做準(zhǔn)備
                        e.value = null; // Help the GC
                    } else {
                        // 如果不是過期實體,則根據(jù)新的長度重新計算存儲位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                       // 將該實體存儲在對應(yīng)ThreadLocal的最后一個位置
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            // 重新分配位置完畢,則重新計算閾值Threshold
            setThreshold(newLen);
            // 記錄實際存儲元素個數(shù)
            size = count;
            // 將新的哈希表賦值至底層table
            table = newTab;
        }

ThreadLocal的remove()操作實際是調(diào)用ThreadLocalMap的remove(ThreadLocal<?> key)方法,該方法進行了如下操作:

  • 1.獲取對應(yīng)的底層哈希表 table,計算對應(yīng)threalocal的存儲位置。
  • 2.循環(huán)遍歷table對應(yīng)該位置的實體,查找對應(yīng)的threadLocal。
  • 3 .獲取當(dāng)前位置的threadLocal,如果key threadLocal一致,則證明找到對應(yīng)的threadLocal,執(zhí)行刪除操作,刪除此位置的實體,結(jié)束。

示例代碼:

/**
         * 移除對應(yīng)ThreadLocal的實體
         */
        private void remove(ThreadLocal<?> key) {
            // 獲取對應(yīng)的底層哈希表 table
            Entry[] tab = table;
            // 獲取哈希表長度
            int len = tab.length;
            // 計算對應(yīng)threalocal的存儲位置
            int i = key.threadLocalHashCode & (len-1);
            // 循環(huán)遍歷table對應(yīng)該位置的實體,查找對應(yīng)的threadLocal
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                // 如果key threadLocal一致,則證明找到對應(yīng)的threadLocal
                if (e.get() == key) {
                    // 執(zhí)行清除操作
                    e.clear();
                    // 清除此位置的實體
                    expungeStaleEntry(i);
                    // 結(jié)束
                    return;
                }
            }
        }

5.ThreadLocalMap中的存儲實體Entry使用ThreadLocal作為key,但這個Entry是繼承弱引用WeakReference的,為什么要這樣設(shè)計,使用了弱引用WeakReference會造成內(nèi)存泄露問題嗎?

首先,回答這個問題之前,我需要解釋一下什么是強引用,什么是弱引用。
我們在正常情況下,普遍使用的是強引用:

A a = new A();

B b = new B();

當(dāng) a = null;b = null;時,一段時間后,JAVA垃圾回收機制GC會將 a 和 b 對應(yīng)所分配的內(nèi)存空間給回收。

但考慮這樣一種情況:

C c = new C(b);
b = null;

當(dāng) b 被設(shè)置成null時,那么是否意味這一段時間后GC工作可以回收 b 所分配的內(nèi)存空間呢?答案是否定的,因為即使 b 被設(shè)置成null,但 c 仍然持有對 b 的引用,而且還是強引用,所以GC不會回收 b 原先所分配的空間,既不能回收,又不能使用,這就造成了 內(nèi)存泄露。

那么如何處理呢?

可以通過c = null;,也可以使用弱引用WeakReference w = new WeakReference(b);。因為使用了弱引用WeakReference,GC是可以回收 b 原先所分配的空間的。
詳解請見:ThreadLocal 內(nèi)存泄露的實例分析

6.ThreadLocal和synchronized的區(qū)別?

ThreadLocal和synchronized關(guān)鍵字都用于處理多線程并發(fā)訪問變量的問題,只是二者處理問題的角度和思路不同。

  • ThreadLocal是一個Java類,通過對當(dāng)前線程中的局部變量的操作來解決不同線程的變量訪問的沖突問題。所以,ThreadLocal提供了線程安全的共享對象機制,每個線程都擁有其副本。
  • Java中的synchronized是一個保留字,它依靠JVM的鎖機制來實現(xiàn)臨界區(qū)的函數(shù)或者變量的訪問中的原子性。在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。此時,被用作“鎖機制”的變量時多個線程共享的。
  • 同步機制(synchronized關(guān)鍵字)采用了以“時間換空間”的方式,提供一份變量,讓不同的線程排隊訪問。而ThreadLocal采用了“以空間換時間”的方式,為每一個線程都提供一份變量的副本,從而實現(xiàn)同時訪問而互不影響。

7.ThreadLocal在現(xiàn)時有什么應(yīng)用場景?

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

  • 解決并發(fā)問題:使用ThreadLocal代替synchronized來保證線程安全。同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
  • 解決數(shù)據(jù)存儲問題:ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,所以每個線程可以訪問自己內(nèi)部的副本變量,不同線程之間不會互相干擾。如一個Parameter對象的數(shù)據(jù)需要在多個模塊中使用,如果采用參數(shù)傳遞的方式,顯然會增加模塊之間的耦合性。此時我們可以使用ThreadLocal解決。

應(yīng)用場景:

  • Spring使用ThreadLocal解決線程安全問題
    我們知道在一般情況下,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進行處理,讓它們也成為線程安全的狀態(tài),因為有狀態(tài)的Bean就可以在多線程中共享了。
  • 一般的Web應(yīng)用劃分為展現(xiàn)層、服務(wù)層和持久層三個層次,在不同的層中編寫對應(yīng)的邏輯,下層通過接口向上層開放功能調(diào)用。在一般情況下,從接收請求到返回響應(yīng)所經(jīng)過的所有程序調(diào)用都同屬于一個線程ThreadLocal是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決了變量并發(fā)訪問的沖突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結(jié)果程序擁有更高的并發(fā)性
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");

·····
}

總結(jié)

  • ThreadLocal提供線程內(nèi)部的局部變量,在本線程內(nèi)隨時隨地可取,隔離其他線程。
  • ThreadLocal的設(shè)計是:每個Thread維護一個ThreadLocalMap哈希表,這個哈希表的key是ThreadLocal實例本身,value才是真正要存儲的值Object。
  • 對ThreadLocal的常用操作實際是對線程Thread中的ThreadLocalMap進行操作。
  • ThreadLocalMap的底層實現(xiàn)是一個定制的自定義HashMap哈希表,ThreadLocalMap的閾值threshold = 底層哈希表table的長度 len * 2 / 3,當(dāng)實際存儲元素個數(shù)size 大于或等于 閾值threshold的 3/4 時size >= threshold*3/4,則對底層哈希表數(shù)組table進行擴容操作。
  • ThreadLocalMap中的哈希表Entry[] table存儲的核心元素是Entry,存儲的key是ThreadLocal實例對象,value是ThreadLocal 對應(yīng)儲存的值value。需要注意的是,此Entry繼承了弱引用 WeakReference,所以在使用ThreadLocalMap時,發(fā)現(xiàn)key == null,則意味著此key ThreadLocal不在被引用,需要將其從ThreadLocalMap哈希表中移除。
  • ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統(tǒng) GC 的時候,這個ThreadLocal勢必會被回收。所以,在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。如果我們不主動調(diào)用上述操作,則會導(dǎo)致內(nèi)存泄露。
  • 為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal后都要調(diào)用remove()來清理無用的Entry。這在操作在使用線程池時尤為重要。
  • ThreadLocal和synchronized的區(qū)別:同步機制(synchronized關(guān)鍵字)采用了以“時間換空間”的方式,提供一份變量,讓不同的線程排隊訪問。而ThreadLocal采用了“以空間換時間”的方式,為每一個線程都提供一份變量的副本,從而實現(xiàn)同時訪問而互不影響。
  • ThreadLocal主要是解決2種類型的問題:A. 解決并發(fā)問題:使用ThreadLocal代替同步機制解決并發(fā)問題。B. 解決數(shù)據(jù)存儲問題:如一個Parameter對象的數(shù)據(jù)需要在多個模塊中使用,如果采用參數(shù)傳遞的方式,顯然會增加模塊之間的耦合性。此時我們可以使用ThreadLocal解決。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容