JAVA并發(fā)-自問自答學(xué)ThreadLocal

前言

ThreadLocal很多同學(xué)都搞不懂是什么東西训裆,可以用來干嘛撼唾。但面試時(shí)卻又經(jīng)常問到坎背,所以這次我和大家一起學(xué)習(xí)ThreadLocal這個(gè)類柜思。

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

問答內(nèi)容

1.

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

答:

  • 從JAVA官方對(duì)ThreadLocal類的說明定義(定義在示例代碼中):ThreadLocal類用來提供線程內(nèi)部的局部變量琅锻。這種變量在多線程環(huán)境下訪問(通過getset方法訪問)時(shí)能保證各個(gè)線程的變量相對(duì)獨(dú)立于其他線程內(nèi)的變量。ThreadLocal實(shí)例通常來說都是private static類型的向胡,用于關(guān)聯(lián)線程和線程上下文恼蓬。

  • 我們可以得知ThreadLocal的作用是:ThreadLocal的作用是提供線程內(nèi)的局部變量,不同的線程之間不會(huì)相互干擾僵芹,這種變量在線程的生命周期內(nèi)起作用处硬,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量的傳遞的復(fù)雜度。

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

示例代碼:


/**
 * 該類提供了線程局部 (thread-local) 變量件豌。 這些變量不同于它們的普通對(duì)應(yīng)物疮方,
 * 因?yàn)樵L問某個(gè)變量(通過其 get 或 set 方法)的每個(gè)線程都有自己的局部變量
 * 它獨(dú)立于變量的初始化副本。ThreadLocal 實(shí)例通常是類中的 private static 字段
 * 它們希望將狀態(tài)與某一個(gè)線程(例如茧彤,用戶 ID 或事務(wù) ID)相關(guān)聯(lián)骡显。
 *
 * 例如,以下類生成對(duì)每個(gè)線程唯一的局部標(biāo)識(shí)符。
 * 
 * 線程 ID 是在第一次調(diào)用 UniqueThreadIdGenerator.getCurrentThreadId() 時(shí)分配的惫谤,
 * 在后續(xù)調(diào)用中不會(huì)更改壁顶。
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // 原子性整數(shù),包含下一個(gè)分配的線程Thread ID 
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // 每一個(gè)線程對(duì)應(yīng)的Thread ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // 返回當(dāng)前線程對(duì)應(yīng)的唯一Thread ID, 必要時(shí)會(huì)進(jìn)行分配
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * 每個(gè)線程都保持對(duì)其線程局部變量副本的隱式引用溜歪,只要線程是活動(dòng)的并且 ThreadLocal 實(shí)例是可訪問的
 * 在線程消失之后若专,其線程局部實(shí)例的所有副本都會(huì)被垃圾回收,(除非存在對(duì)這些副本的其他引用)蝴猪。
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
·····
   /**
     * 自定義哈希碼(僅在ThreadLocalMaps中有用)
     * 可用于降低hash沖突
     */
    private final int threadLocalHashCode = nextHashCode();

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


    /**
     * 表示了連續(xù)分配的兩個(gè)ThreadLocal實(shí)例的threadLocalHashCode值的增量 
     */
    private static final int HASH_INCREMENT = 0x61c88647;


    /**
     * 返回下一個(gè)哈希碼hashCode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
·····

}
  • 其中nextHashCode()方法就是一個(gè)原子類不停地去加上0x61c88647调衰,這是一個(gè)很特別的數(shù),叫斐波那契散列(Fibonacci Hashing)拯腮,斐波那契又有一個(gè)名稱叫黃金分割窖式,也就是說將這個(gè)數(shù)作為哈希值的增量將會(huì)使哈希表的分布更為均勻。

2.

問:ThreadLocal實(shí)現(xiàn)原理是什么动壤,它是怎么樣做到局部變量不同的線程之間不會(huì)相互干擾的萝喘?

答:

  • 通常,如果我不去看源代碼的話琼懊,我猜ThreadLocal是這樣子設(shè)計(jì)的:每個(gè)ThreadLocal類都創(chuàng)建一個(gè)Map阁簸,然后用線程的ID threadID作為Mapkey,要存儲(chǔ)的局部變量作為Mapvalue哼丈,這樣就能達(dá)到各個(gè)線程的值隔離的效果启妹。這是最簡(jiǎn)單的設(shè)計(jì)方法,JDK最早期的ThreadLocal就是這樣設(shè)計(jì)的醉旦。

  • 但是饶米,JDK后面優(yōu)化了設(shè)計(jì)方案,現(xiàn)時(shí)JDK8 ThreadLocal的設(shè)計(jì)是:每個(gè)Thread維護(hù)一個(gè)ThreadLocalMap哈希表车胡,這個(gè)哈希表的keyThreadLocal實(shí)例本身檬输,value才是真正要存儲(chǔ)的值Object

  • 這個(gè)設(shè)計(jì)與我們一開始說的設(shè)計(jì)剛好相反匈棘,這樣設(shè)計(jì)有如下幾點(diǎn)優(yōu)勢(shì):

    1) 這樣設(shè)計(jì)之后每個(gè)Map存儲(chǔ)的Entry數(shù)量就會(huì)變小丧慈,因?yàn)橹暗拇鎯?chǔ)數(shù)量由Thread的數(shù)量決定,現(xiàn)在是由ThreadLocal的數(shù)量決定主卫。

    2) 當(dāng)Thread銷毀之后逃默,對(duì)應(yīng)的ThreadLocalMap也會(huì)隨之銷毀,能減少內(nèi)存的使用簇搅。

ThreadLocal引用關(guān)系圖- 圖片來自于《簡(jiǎn)書 - 對(duì)ThreadLocal實(shí)現(xiàn)原理的一點(diǎn)思考》

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

3.

問:您能說說ThreadLocal常用操作的底層實(shí)現(xiàn)原理嗎完域?如存儲(chǔ)set(T value),獲取get()瘩将,刪除remove()等操作筒主。

答:

  • 調(diào)用get()操作獲取ThreadLocal中對(duì)應(yīng)當(dāng)前線程存儲(chǔ)的值時(shí)关噪,進(jìn)行了如下操作:

    1 ) 獲取當(dāng)前線程Thread對(duì)象,進(jìn)而獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象乌妙。

    2 ) 判斷當(dāng)前的ThreadLocalMap是否存在:

  • 如果存在,則以當(dāng)前的ThreadLocalkey建钥,調(diào)用ThreadLocalMap中的getEntry方法獲取對(duì)應(yīng)的存儲(chǔ)實(shí)體 e藤韵。找到對(duì)應(yīng)的存儲(chǔ)實(shí)體 e,獲取存儲(chǔ)實(shí)體 e 對(duì)應(yīng)的 value值熊经,即為我們想要的當(dāng)前線程對(duì)應(yīng)此ThreadLocal的值泽艘,返回結(jié)果值澎迎。

  • 如果不存在础嫡,則證明此線程沒有維護(hù)的ThreadLocalMap對(duì)象骇陈,調(diào)用setInitialValue方法進(jìn)行初始化男旗。返回setInitialValue初始化的值氛什。

  • setInitialValue方法的操作如下:

    1 ) 調(diào)用initialValue獲取初始化的值憾股。

    2 ) 獲取當(dāng)前線程Thread對(duì)象英遭,進(jìn)而獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象掷伙。

    3 ) 判斷當(dāng)前的ThreadLocalMap是否存在:

  • 如果存在务唐,則調(diào)用map.set設(shè)置此實(shí)體entry雳攘。

  • 如果不存在,則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化枫笛,并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中吨灭。

PS:關(guān)于ThreadLocalMap對(duì)應(yīng)的相關(guān)操作,放在下一個(gè)問題詳細(xì)說明刑巧。

示例代碼:

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

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

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

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

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

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

    1 ) 獲取當(dāng)前線程Thread對(duì)象,進(jìn)而獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象。

    2 ) 判斷當(dāng)前的ThreadLocalMap是否存在:

  • 如果存在困肩,則調(diào)用map.set設(shè)置此實(shí)體entry划纽。

  • 如果不存在,則調(diào)用createMap進(jìn)行ThreadLocalMap對(duì)象的初始化锌畸,并將此實(shí)體entry作為第一個(gè)值存放至ThreadLocalMap中勇劣。

示例代碼:

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

    /**
     * 為當(dāng)前線程Thread 創(chuàng)建對(duì)應(yīng)維護(hù)的ThreadLocalMap. 
     *
     * @param t the current thread 當(dāng)前線程
     * @param firstValue 第一個(gè)要存放的ThreadLocal變量值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 調(diào)用remove()操作刪除ThreadLocal中對(duì)應(yīng)當(dāng)前線程已存儲(chǔ)的值時(shí)潭枣,進(jìn)行了如下操作:

    1 ) 獲取當(dāng)前線程Thread對(duì)象比默,進(jìn)而獲取此線程對(duì)象中維護(hù)的ThreadLocalMap對(duì)象。

    2 ) 判斷當(dāng)前的ThreadLocalMap是否存在盆犁, 如果存在命咐,則調(diào)用map.remove,以當(dāng)前ThreadLocalkey刪除對(duì)應(yīng)的實(shí)體entry谐岁。

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

4.

問:對(duì)ThreadLocal的常用操作實(shí)際是對(duì)線程Thread中的ThreadLocalMap進(jìn)行操作,核心是ThreadLocalMap這個(gè)哈希表翰铡,你能談?wù)?code>ThreadLocalMap的內(nèi)部底層實(shí)現(xiàn)嗎?

答:

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

    1 ) Entry[] table;:底層哈希表 table, 必要時(shí)需要進(jìn)行擴(kuò)容,底層哈希表 table.length 長(zhǎng)度必須是2的n次方锭魔。

    2 ) int size;:實(shí)際存儲(chǔ)鍵值對(duì)元素個(gè)數(shù) entries

    3 ) int threshold;:下一次擴(kuò)容時(shí)的閾值例证,閾值 threshold = 底層哈希表table的長(zhǎng)度 len * 2 / 3。當(dāng)size >= threshold時(shí)迷捧,遍歷table并刪除keynull的元素织咧,如果刪除后size >= threshold*3/4時(shí),需要對(duì)table進(jìn)行擴(kuò)容(詳情請(qǐng)查看set(ThreadLocal<?> key, Object value)方法說明)漠秋。

  • 其中Entry[] table;哈希表存儲(chǔ)的核心元素是Entry笙蒙,Entry包含:

    1 ) ThreadLocal<?> k;:當(dāng)前存儲(chǔ)的ThreadLocal實(shí)例對(duì)象

    2 ) Object value;:當(dāng)前 ThreadLocal 對(duì)應(yīng)儲(chǔ)存的值value

  • 需要注意的是庆锦,此Entry繼承了弱引用 WeakReference捅位,所以在使用ThreadLocalMap時(shí),發(fā)現(xiàn)key == null搂抒,則意味著此key ThreadLocal不在被引用艇搀,需要將其從ThreadLocalMap哈希表中移除。(弱引用相關(guān)問題解釋請(qǐng)查看 問答 5)

示例代碼:

    /**
     * ThreadLocalMap 是一個(gè)定制的自定義 hashMap 哈希表求晶,只適合用于維護(hù)
     * 線程對(duì)應(yīng)ThreadLocal的值. 此類的方法沒有在ThreadLocal 類外部暴露焰雕,
     * 此類是私有的,允許在 Thread 類中以字段的形式聲明 芳杏,     
     * 以助于處理存儲(chǔ)量大矩屁,生命周期長(zhǎng)的使用用途辟宗,
     * 此類定制的哈希表實(shí)體鍵值對(duì)使用弱引用WeakReferences 作為key, 
     * 但是, 一旦引用不在被使用吝秕,
     * 只有當(dāng)哈希表中的空間被耗盡時(shí)泊脐,對(duì)應(yīng)不再使用的鍵值對(duì)實(shí)體才會(huì)確保被 移除回收。
     */
    static class ThreadLocalMap {

        /**
         * 實(shí)體entries在此hash map中是繼承弱引用 WeakReference, 
         * 使用ThreadLocal 作為 key 鍵.  請(qǐng)注意烁峭,當(dāng)key為null(i.e. entry.get()
         * == null) 意味著此key不再被引用,此時(shí)實(shí)體entry 會(huì)從哈希表中刪除晨抡。
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 當(dāng)前 ThreadLocal 對(duì)應(yīng)儲(chǔ)存的值value. */
            Object value;

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

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

        /**
         * 底層哈希表 table, 必要時(shí)需要進(jìn)行擴(kuò)容.
         * 底層哈希表 table.length 長(zhǎng)度必須是2的n次方.
         */
        private Entry[] table;

        /**
         * 實(shí)際存儲(chǔ)鍵值對(duì)元素個(gè)數(shù) entries.
         */
        private int size = 0;

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

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

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

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

    }
  • ThreadLocalMap的構(gòu)造方法是延遲加載的,也就是說则剃,只有當(dāng)線程需要存儲(chǔ)對(duì)應(yīng)的ThreadLocal的值時(shí),才初始化創(chuàng)建一次(僅初始化一次)如捅。初始化步驟如下:

    1) 初始化底層數(shù)組table的初始容量為 16棍现。

    2) 獲取ThreadLocal中的threadLocalHashCode,通過threadLocalHashCode & (INITIAL_CAPACITY - 1)镜遣,即ThreadLocal 的 hash 值 threadLocalHashCode % 哈希表的長(zhǎng)度 length 的方式計(jì)算該實(shí)體的存儲(chǔ)位置己肮。

    3) 存儲(chǔ)當(dāng)前的實(shí)體,key 為 : 當(dāng)前ThreadLocal value:真正要存儲(chǔ)的值

    4)設(shè)置當(dāng)前實(shí)際存儲(chǔ)元素個(gè)數(shù) size 為 1

    5)設(shè)置閾值setThreshold(INITIAL_CAPACITY)悲关,為初始化容量 16 的 2/3谎僻。

示例代碼:


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

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

    1 ) 計(jì)算要獲取的entry的存儲(chǔ)位置,存儲(chǔ)位置計(jì)算等價(jià)于:ThreadLocalhashthreadLocalHashCode % 哈希表的長(zhǎng)度 length崔涂。

    2 ) 根據(jù)計(jì)算的存儲(chǔ)位置阳掐,獲取到對(duì)應(yīng)的實(shí)體 Entry。判斷對(duì)應(yīng)實(shí)體Entry是否存在 并且 key是否相等:

  • 存在對(duì)應(yīng)實(shí)體Entry并且對(duì)應(yīng)key相等冷蚂,即同一ThreadLocal缭保,返回對(duì)應(yīng)的實(shí)體Entry

  • 不存在對(duì)應(yīng)實(shí)體Entry 或者 key不相等帝雇,則通過調(diào)用getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法繼續(xù)查找涮俄。

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

    1 ) 獲取底層哈希表數(shù)組table,循環(huán)遍歷對(duì)應(yīng)要查找的實(shí)體Entry所關(guān)聯(lián)的位置尸闸。

    2 ) 獲取當(dāng)前遍歷的entrykey ThreadLocal彻亲,比較key是否一致孕锄,一致則返回。

    3 ) 如果key不一致 并且 keynull苞尝,則證明引用已經(jīng)不存在畸肆,這是因?yàn)?code>Entry繼承的是WeakReference,這是弱引用帶來的坑宙址。調(diào)用expungeStaleEntry(int staleSlot)方法刪除過期的實(shí)體Entry(此方法不單獨(dú)解釋轴脐,請(qǐng)查看示例代碼,有詳細(xì)注釋說明)抡砂。

    4 ) key不一致 大咱,key也不為空,則遍歷下一個(gè)位置注益,繼續(xù)查找碴巾。

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

示例代碼:


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

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


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

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

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

                        // 我們必須一直遍歷直到最后
                        // 因?yàn)檫€可能存在多個(gè)過期的實(shí)體
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * 刪除所有過期的實(shí)體
         */
        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)操作實(shí)際是調(diào)用ThreadLocalMapset(ThreadLocal<?> key, Object value)方法泽西,該方法進(jìn)行了如下操作:

    1 ) 獲取對(duì)應(yīng)的底層哈希表table曹铃,計(jì)算對(duì)應(yīng)threalocal的存儲(chǔ)位置。

    2 ) 循環(huán)遍歷table對(duì)應(yīng)該位置的實(shí)體捧杉,查找對(duì)應(yīng)的threadLocal陕见。

    3 ) 獲取當(dāng)前位置的threadLocal秘血,如果key threadLocal一致,則證明找到對(duì)應(yīng)的threadLocal评甜,將新值賦值給找到的當(dāng)前實(shí)體Entryvalue中灰粮,結(jié)束。

    4 ) 如果當(dāng)前位置的key threadLocal不一致忍坷,并且key threadLocalnull粘舟,則調(diào)用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法(此方法不單獨(dú)解釋,請(qǐng)查看示例代碼佩研,有詳細(xì)注釋說明)柑肴,替換該位置key == null 的實(shí)體為當(dāng)前要設(shè)置的實(shí)體,結(jié)束旬薯。

    5 ) 如果當(dāng)前位置的key threadLocal不一致嘉抒,并且key threadLocal不為null,則創(chuàng)建新的實(shí)體袍暴,并存放至當(dāng)前位置 i tab[i] = new Entry(key, value);,實(shí)際存儲(chǔ)鍵值對(duì)元素個(gè)數(shù)size + 1隶症,由于弱引用帶來了這個(gè)問題政模,所以要調(diào)用cleanSomeSlots(int i, int n)方法清除無用數(shù)據(jù)(此方法不單獨(dú)解釋,請(qǐng)查看示例代碼蚂会,有詳細(xì)注釋說明)淋样,才能判斷現(xiàn)在的size有沒有達(dá)到閥值threshhold,如果沒有要清除的數(shù)據(jù)胁住,存儲(chǔ)元素個(gè)數(shù)仍然 大于 閾值 則調(diào)用rehash方法進(jìn)行擴(kuò)容(此方法不單獨(dú)解釋趁猴,請(qǐng)查看示例代碼,有詳細(xì)注釋說明)彪见。

示例代碼:


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

            // 我們不會(huì)像get()方法那樣使用快速設(shè)置的方式儡司,
            // 因?yàn)橥ǔ:苌偈褂胹et()方法去創(chuàng)建新的實(shí)體
            // 相對(duì)于替換一個(gè)已經(jīng)存在的實(shí)體, 在這種情況下,
            // 快速設(shè)置方案會(huì)經(jīng)常失敗。

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

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

            // 往前找蜡吧,找到table中第一個(gè)過期的實(shí)體的下標(biāo)
            // 清理整個(gè)table是為了避免因?yàn)槔厥諑淼倪B續(xù)增長(zhǎng)哈希的危險(xiǎn)
            // 也就是說毫蚓,哈希表沒有清理干凈,當(dāng)GC到來的時(shí)候斩跌,后果很嚴(yán)重

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

            // 找到key一致的ThreadLocal或找到一個(gè)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è)置的實(shí)體放置在此過期的實(shí)體的位置上
                if (k == key) {
                    // 替換柬批,將要設(shè)置的值放在此過期的實(shí)體中
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

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

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

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

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


        /**
         * 啟發(fā)式的掃描查找一些過期的實(shí)體并清除上沐,
         * 此方法會(huì)再添加新實(shí)體的時(shí)候被調(diào)用, 
         * 或者過期的元素被清除時(shí)也會(huì)被調(diào)用.
         * 如果實(shí)在沒有過期數(shù)據(jù),那么這個(gè)算法的時(shí)間復(fù)雜度就是O(log n)
         * 如果有過期數(shù)據(jù)楞艾,那么這個(gè)算法的時(shí)間復(fù)雜度就是O(n)
         * 
         * @param i 一個(gè)確定不是過期的實(shí)體的位置参咙,從這個(gè)位置i開始掃描
         *
         * @param n 掃描控制: 有{@code log2(n)} 單元會(huì)被掃描,
         * 除非找到了過期的實(shí)體, 在這種情況下
         * 有{@code log2(table.length)-1} 的格外單元會(huì)被掃描.
         * 當(dāng)調(diào)用插入時(shí), 這個(gè)參數(shù)的值是存儲(chǔ)實(shí)體的個(gè)數(shù),
         * 但如果調(diào)用 replaceStaleEntry方法, 這個(gè)值是哈希表table的長(zhǎng)度
         * (注意: 所有的這些都可能或多或少的影響n的權(quán)重
         * 但是這個(gè)版本簡(jiǎn)單硫眯,快速蕴侧,而且似乎執(zhí)行效率還可以)
         *
         * @return true 返回true,如果有任何過期的實(shí)體被刪除两入。
         */
        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;
        }


        /**
         * 哈希表擴(kuò)容方法
         * 首先掃描整個(gè)哈希表table净宵,刪除過期的實(shí)體
         * 縮小哈希表table大小 或 擴(kuò)大哈希表table大小,擴(kuò)大的容量是加倍.
         */
        private void rehash() {
            // 刪除所有過期的實(shí)體
            expungeStaleEntries();

            // 使用較低的閾值threshold加倍以避免滯后
            // 存儲(chǔ)實(shí)體個(gè)數(shù) 大于等于 閾值的3/4則擴(kuò)容
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * 擴(kuò)容方法裹纳,以2倍的大小進(jìn)行擴(kuò)容
         * 擴(kuò)容的思想跟HashMap很相似择葡,都是把容量擴(kuò)大兩倍
         * 不同之處還是因?yàn)閃eakReference帶來的
         */
        private void resize() {
            // 記錄舊的哈希表
            Entry[] oldTab = table;
            // 記錄舊的哈希表長(zhǎng)度
            int oldLen = oldTab.length;
            // 新的哈希表長(zhǎng)度為舊的哈希表長(zhǎng)度的2倍
            int newLen = oldLen * 2;
            // 創(chuàng)建新的哈希表
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            // 逐一遍歷舊的哈希表table的每個(gè)實(shí)體,重新分配至新的哈希表中
            for (int j = 0; j < oldLen; ++j) {
                // 獲取對(duì)應(yīng)位置的實(shí)體
                Entry e = oldTab[j];
                // 如果實(shí)體不會(huì)null
                if (e != null) {
                    // 獲取實(shí)體對(duì)應(yīng)的ThreadLocal
                    ThreadLocal<?> k = e.get(); 
                    // 如果該ThreadLocal 為 null
                    if (k == null) {
                        // 則對(duì)應(yīng)的值也要清除
                        // 就算是擴(kuò)容剃氧,也不能忘了為擦除過期數(shù)據(jù)做準(zhǔn)備
                        e.value = null; // Help the GC
                    } else {
                        // 如果不是過期實(shí)體敏储,則根據(jù)新的長(zhǎng)度重新計(jì)算存儲(chǔ)位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                       // 將該實(shí)體存儲(chǔ)在對(duì)應(yīng)ThreadLocal的最后一個(gè)位置
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            // 重新分配位置完畢,則重新計(jì)算閾值Threshold
            setThreshold(newLen);
            // 記錄實(shí)際存儲(chǔ)元素個(gè)數(shù)
            size = count;
            // 將新的哈希表賦值至底層table
            table = newTab;
        }
  • ThreadLocalremove()操作實(shí)際是調(diào)用ThreadLocalMapremove(ThreadLocal<?> key)方法朋鞍,該方法進(jìn)行了如下操作:

    1 ) 獲取對(duì)應(yīng)的底層哈希表 table虹曙,計(jì)算對(duì)應(yīng)threalocal的存儲(chǔ)位置。

    2 ) 循環(huán)遍歷table對(duì)應(yīng)該位置的實(shí)體番舆,查找對(duì)應(yīng)的threadLocal酝碳。

    3 ) 獲取當(dāng)前位置的threadLocal,如果key threadLocal一致恨狈,則證明找到對(duì)應(yīng)的threadLocal疏哗,執(zhí)行刪除操作,刪除此位置的實(shí)體禾怠,結(jié)束返奉。

示例代碼:


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

5.

問:ThreadLocalMap中的存儲(chǔ)實(shí)體Entry使用ThreadLocal作為key芽偏,但這個(gè)Entry是繼承弱引用WeakReference的雷逆,為什么要這樣設(shè)計(jì),使用了弱引用WeakReference會(huì)造成內(nèi)存泄露問題嗎污尉?

答:

  • 首先膀哲,回答這個(gè)問題之前,我需要解釋一下什么是強(qiáng)引用被碗,什么是弱引用某宪。

我們?cè)谡G闆r下,普遍使用的是強(qiáng)引用:

A a = new A();

B b = new B();

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

但考慮這樣一種情況:

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

當(dāng) b 被設(shè)置成null時(shí)焚志,那么是否意味這一段時(shí)間后GC工作可以回收 b 所分配的內(nèi)存空間呢衣迷?答案是否定的,因?yàn)榧词?b 被設(shè)置成null酱酬,但 c 仍然持有對(duì) b 的引用蘑险,而且還是強(qiáng)引用,所以GC不會(huì)回收 b 原先所分配的空間岳悟,既不能回收,又不能使用泼差,這就造成了 內(nèi)存泄露贵少。

那么如何處理呢?

可以通過c = null;堆缘,也可以使用弱引用WeakReference w = new WeakReference(b);滔灶。因?yàn)槭褂昧巳跻?code>WeakReference,GC是可以回收 b 原先所分配的空間的吼肥。

上述解釋主要參考自:對(duì)ThreadLocal實(shí)現(xiàn)原理的一點(diǎn)思考

  • 回到ThreadLocal的層面上录平,ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用來引用它缀皱,那么系統(tǒng) GC 的時(shí)候斗这,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來啤斗,ThreadLocalMap中就會(huì)出現(xiàn)keynullEntry表箭,就沒有辦法訪問這些keynullEntryvalue,如果當(dāng)前線程再遲遲不結(jié)束的話钮莲,這些keynullEntryvalue就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠(yuǎn)無法回收免钻,造成內(nèi)存泄漏彼水。

其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況极舔,也加上了一些防護(hù)措施:在ThreadLocalget(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有keynullvalue凤覆。

但是這些被動(dòng)的預(yù)防措施并不能保證不會(huì)內(nèi)存泄漏:

  • 使用staticThreadLocal,延長(zhǎng)了ThreadLocal的生命周期拆魏,可能導(dǎo)致的內(nèi)存泄漏(參考ThreadLocal 內(nèi)存泄露的實(shí)例分析)盯桦。

  • 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法,那么就會(huì)導(dǎo)致內(nèi)存泄漏稽揭。

從表面上看內(nèi)存泄漏的根源在于使用了弱引用俺附。網(wǎng)上的文章大多著重分析ThreadLocal使用了弱引用會(huì)導(dǎo)致內(nèi)存泄漏,但是另一個(gè)問題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用溪掀?

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

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

為了應(yīng)對(duì)非常大和長(zhǎng)時(shí)間的用途事镣,哈希表使用弱引用的 key

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

  • key 使用強(qiáng)引用:引用的ThreadLocal的對(duì)象被回收了揪胃,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用璃哟,如果沒有手動(dòng)刪除,ThreadLocal不會(huì)被回收喊递,導(dǎo)致Entry內(nèi)存泄漏随闪。

  • key 使用弱引用:引用的ThreadLocal的對(duì)象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用骚勘,即使沒有手動(dòng)刪除铐伴,ThreadLocal也會(huì)被回收。value在下一次ThreadLocalMap調(diào)用get(),set(),remove()的時(shí)候會(huì)被清除俏讹。

  • 比較兩種情況当宴,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果都沒有手動(dòng)刪除對(duì)應(yīng)key泽疆,都會(huì)導(dǎo)致內(nèi)存泄漏户矢,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會(huì)內(nèi)存泄漏,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用get(),set(),remove()的時(shí)候會(huì)被清除殉疼。

因此梯浪,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果沒有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏瓢娜,而不是因?yàn)槿跻谩?/p>

綜合上面的分析挂洛,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果,那么怎么避免內(nèi)存泄漏呢眠砾?

每次使用完ThreadLocal抹锄,都調(diào)用它的remove()方法,清除數(shù)據(jù)。

在使用線程池的情況下伙单,沒有及時(shí)清理ThreadLocal获高,不僅是內(nèi)存泄漏的問題,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題吻育。所以念秧,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理布疼。

上述解釋主要參考自:深入分析 ThreadLocal 內(nèi)存泄漏問題

6.

問:ThreadLocalsynchronized的區(qū)別?

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

  1. ThreadLocal是一個(gè)Java類,通過對(duì)當(dāng)前線程中的局部變量的操作來解決不同線程的變量訪問的沖突問題游两。所以砾层,ThreadLocal提供了線程安全的共享對(duì)象機(jī)制,每個(gè)線程都擁有其副本贱案。

  2. Java中的synchronized是一個(gè)保留字肛炮,它依靠JVM的鎖機(jī)制來實(shí)現(xiàn)臨界區(qū)的函數(shù)或者變量的訪問中的原子性。在同步機(jī)制中宝踪,通過對(duì)象的鎖機(jī)制保證同一時(shí)間只有一個(gè)線程訪問變量侨糟。此時(shí),被用作“鎖機(jī)制”的變量時(shí)多個(gè)線程共享的瘩燥。

  • 同步機(jī)制(synchronized關(guān)鍵字)采用了以“時(shí)間換空間”的方式秕重,提供一份變量,讓不同的線程排隊(duì)訪問厉膀。而ThreadLocal采用了“以空間換時(shí)間”的方式溶耘,為每一個(gè)線程都提供一份變量的副本,從而實(shí)現(xiàn)同時(shí)訪問而互不影響服鹅。

7.

問:ThreadLocal在現(xiàn)時(shí)有什么應(yīng)用場(chǎng)景凳兵?

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

  • 解決并發(fā)問題:使用ThreadLocal代替synchronized來保證線程安全。同步機(jī)制采用了“以時(shí)間換空間”的方式菱魔,而ThreadLocal采用了“以空間換時(shí)間”的方式。前者僅提供一份變量吟孙,讓不同的線程排隊(duì)訪問澜倦,而后者為每一個(gè)線程都提供了一份變量,因此可以同時(shí)訪問而互不影響杰妓。

  • 解決數(shù)據(jù)存儲(chǔ)問題:ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本藻治,所以每個(gè)線程可以訪問自己內(nèi)部的副本變量,不同線程之間不會(huì)互相干擾巷挥。如一個(gè)Parameter對(duì)象的數(shù)據(jù)需要在多個(gè)模塊中使用桩卵,如果采用參數(shù)傳遞的方式,顯然會(huì)增加模塊之間的耦合性。此時(shí)我們可以使用ThreadLocal解決雏节。

應(yīng)用場(chǎng)景:

Spring使用ThreadLocal解決線程安全問題

  • 我們知道在一般情況下胜嗓,只有無狀態(tài)的Bean才可以在多線程環(huán)境下共享,在Spring中钩乍,絕大部分Bean都可以聲明為singleton作用域辞州。就是因?yàn)?code>Spring對(duì)一些Bean(如RequestContextHolderTransactionSynchronizationManager寥粹、LocaleContextHolder等)中非線程安全狀態(tài)采用ThreadLocal進(jìn)行處理变过,讓它們也成為線程安全的狀態(tài),因?yàn)橛袪顟B(tài)的Bean就可以在多線程中共享了涝涤。

  • 一般的Web應(yīng)用劃分為展現(xiàn)層媚狰、服務(wù)層和持久層三個(gè)層次,在不同的層中編寫對(duì)應(yīng)的邏輯阔拳,下層通過接口向上層開放功能調(diào)用崭孤。在一般情況下,從接收請(qǐng)求到返回響應(yīng)所經(jīng)過的所有程序調(diào)用都同屬于一個(gè)線程ThreadLocal是解決線程安全問題一個(gè)很好的思路衫生,它通過為每個(gè)線程提供一個(gè)獨(dú)立的變量副本解決了變量并發(fā)訪問的沖突問題裳瘪。在很多情況下,ThreadLocal比直接使用synchronized同步機(jī)制解決線程安全問題更簡(jiǎn)單罪针,更方便彭羹,且結(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é)

  1. ThreadLocal提供線程內(nèi)部的局部變量泪酱,在本線程內(nèi)隨時(shí)隨地可取派殷,隔離其他線程。

  2. ThreadLocal的設(shè)計(jì)是:每個(gè)Thread維護(hù)一個(gè)ThreadLocalMap哈希表墓阀,這個(gè)哈希表的keyThreadLocal實(shí)例本身毡惜,value才是真正要存儲(chǔ)的值Object

  3. 對(duì)ThreadLocal的常用操作實(shí)際是對(duì)線程Thread中的ThreadLocalMap進(jìn)行操作斯撮。

  4. ThreadLocalMap的底層實(shí)現(xiàn)是一個(gè)定制的自定義HashMap哈希表经伙,ThreadLocalMap的閾值threshold = 底層哈希表table的長(zhǎng)度 len * 2 / 3,當(dāng)實(shí)際存儲(chǔ)元素個(gè)數(shù)size 大于或等于 閾值threshold3/4 時(shí)size >= threshold*3/4勿锅,則對(duì)底層哈希表數(shù)組table進(jìn)行擴(kuò)容操作帕膜。

  5. ThreadLocalMap中的哈希表Entry[] table存儲(chǔ)的核心元素是Entry,存儲(chǔ)的keyThreadLocal實(shí)例對(duì)象溢十,valueThreadLocal 對(duì)應(yīng)儲(chǔ)存的值value垮刹。需要注意的是,此Entry繼承了弱引用 WeakReference张弛,所以在使用ThreadLocalMap時(shí)荒典,發(fā)現(xiàn)key == null酪劫,則意味著此key ThreadLocal不在被引用,需要將其從ThreadLocalMap哈希表中移除寺董。

  6. ThreadLocalMap使用ThreadLocal的弱引用作為key覆糟,如果一個(gè)ThreadLocal沒有外部強(qiáng)引用來引用它,那么系統(tǒng) GC 的時(shí)候螃征,這個(gè)ThreadLocal勢(shì)必會(huì)被回收搪桂。所以,在ThreadLocalget(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有keynullvalue盯滚。如果我們不主動(dòng)調(diào)用上述操作踢械,則會(huì)導(dǎo)致內(nèi)存泄露。

  7. 為了安全地使用ThreadLocal魄藕,必須要像每次使用完鎖就解鎖一樣内列,在每次使用完ThreadLocal后都要調(diào)用remove()來清理無用的Entry。這在操作在使用線程池時(shí)尤為重要背率。

  8. ThreadLocalsynchronized的區(qū)別:同步機(jī)制(synchronized關(guān)鍵字)采用了以“時(shí)間換空間”的方式话瞧,提供一份變量,讓不同的線程排隊(duì)訪問寝姿。而ThreadLocal采用了“以空間換時(shí)間”的方式交排,為每一個(gè)線程都提供一份變量的副本,從而實(shí)現(xiàn)同時(shí)訪問而互不影響饵筑。

  9. ThreadLocal主要是解決2種類型的問題:A. 解決并發(fā)問題:使用ThreadLocal代替同步機(jī)制解決并發(fā)問題埃篓。B. 解決數(shù)據(jù)存儲(chǔ)問題:如一個(gè)Parameter對(duì)象的數(shù)據(jù)需要在多個(gè)模塊中使用,如果采用參數(shù)傳遞的方式根资,顯然會(huì)增加模塊之間的耦合性架专。此時(shí)我們可以使用ThreadLocal解決。

參考文章

深入淺出ThreadLocal
ThreadLocal和synchronized的區(qū)別?
深入剖析ThreadLocal
ThreadLocal內(nèi)部機(jī)制
聊一聊Spring中的線程安全性
對(duì)ThreadLocal實(shí)現(xiàn)原理的一點(diǎn)思考
深入分析 ThreadLocal 內(nèi)存泄漏問題
學(xué)習(xí)Spring必學(xué)的Java基礎(chǔ)知識(shí)(6)----ThreadLocal
ThreadLocal設(shè)計(jì)模式
ThreadLocal案例分析
Spring單例模式與線程安全ThreadLocal

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玄帕,一起剝皮案震驚了整個(gè)濱河市部脚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裤纹,老刑警劉巖委刘,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鹰椒,居然都是意外死亡锡移,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門吹零,熙熙樓的掌柜王于貴愁眉苦臉地迎上來罩抗,“玉大人拉庵,你說我怎么就攤上這事灿椅。” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵茫蛹,是天一觀的道長(zhǎng)操刀。 經(jīng)常有香客問我,道長(zhǎng)婴洼,這世上最難降的妖魔是什么骨坑? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮柬采,結(jié)果婚禮上欢唾,老公的妹妹穿的比我還像新娘。我一直安慰自己粉捻,他們只是感情好礁遣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布沧竟。 她就那樣靜靜地躺著葱色,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锌妻。 梳的紋絲不亂的頭發(fā)上盈包,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天沸呐,我揣著相機(jī)與錄音,去河邊找鬼呢燥。 笑死崭添,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疮茄。 我是一名探鬼主播滥朱,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼力试!你這毒婦竟也來了徙邻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤畸裳,失蹤者是張志新(化名)和其女友劉穎缰犁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怖糊,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡帅容,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伍伤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片并徘。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖扰魂,靈堂內(nèi)的尸體忽然破棺而出麦乞,到底是詐尸還是另有隱情蕴茴,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布姐直,位于F島的核電站倦淀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏声畏。R本人自食惡果不足惜撞叽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望插龄。 院中可真熱鬧愿棋,春花似錦、人聲如沸均牢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膨处。三九已至见秤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間真椿,已是汗流浹背鹃答。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留突硝,地道東北人测摔。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像解恰,于是被迫代替她去往敵國(guó)和親锋八。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android Handler機(jī)制系列文章整體內(nèi)容如下: Android Handler機(jī)制1之ThreadAnd...
    隔壁老李頭閱讀 7,607評(píng)論 4 30
  • 從三月份找實(shí)習(xí)到現(xiàn)在护盈,面了一些公司挟纱,掛了不少,但最終還是拿到小米腐宋、百度紊服、阿里、京東胸竞、新浪欺嗤、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,184評(píng)論 11 349
  • 原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑 詳情請(qǐng)戳www.codercc.com 1. ThreadLoc...
    你聽___閱讀 6,723評(píng)論 8 19
  • 1. 概念 ThreadLocal 用于提供線程局部變量卫枝,在多線程環(huán)境可以保證各個(gè)線程里的變量獨(dú)立于其它線程里的變...
    zly394閱讀 1,701評(píng)論 0 1
  • 爸 水深么[調(diào)皮] 不深 ?剛過膝蓋……[微笑] 爸 我要不是鼻子長(zhǎng)點(diǎn) 可能今天就交代在這兒了煎饼![捂臉] ———...
    昊Ma芳閱讀 487評(píng)論 3 5