ThreadLocal源碼解析,及使用時需要注意項

Java 多線程類庫對於共享數據的讀寫控制主要採用鎖機制保證線程安全,本文所要探究的 ThreadLocal 則採用了一種完全不同的策略。ThreadLocal 不是用來解決共享數據的併發訪問問題的,它讓每個線程都將目標數據複製一份作爲線程私有,後續對於該數據的操作都是在各自私有的副本上進行,線程之間彼此相互隔離,也就不存在競爭問題。

下面的例子演示了 ThreadLocal 的典型應用場景,在 jdk 1.8 之前,如果我們希望對日期和時間進行格式化操作,則需要使用 SimpleDateFormat 類,而我們知道它是是非線程安全的,在多線程併發執行時會出現一些奇怪的問題,而對於該類使用的最佳實踐則是採用 ThreadLocal 進行包裝,以保證每個線程都有一份屬於自己的 SimpleDateFormat 對象,如下所示:

ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

一. 線程安全機制

那麼 ThreadLocal 是怎麼做到讓修飾的對象能夠在每個線程中各持有一份呢?我們先來簡單的概括一下:在 ThreadLocal 中定義了一個靜態內部類 ThreadLocalMap,可以將其理解爲一個特有的 Map 類型,而在 Thread 類中聲明瞭一個 ThreadLocalMap 類型的屬性 threadLocals,所以針對每個 Thread 對象,也就是每個線程來說都包含了一個 ThreadLocalMap 對象,即每個線程都有一個屬於自己的內存數據庫,而數據庫中存儲的就是我們用 ThreadLocal 修飾的對象,這裏的 key 就是對應的 ThreadLocal 對象,而 value 就是我們記錄在 ThreadLocal 中的值。當希望獲取該對象時,我們首先需要拿到當前線程對應的 Thread 對象,然後獲取到該對象對應的 threadLocals 屬性,也就拿到了線程私有的內存數據庫,最後以 ThreadLocal 對象爲 key 獲取到其修飾的目標值。整個過程還是有點繞的,可以藉助下面這幅圖進行理解。

ThreadLocal

1.1 內存數據庫 ThreadLocalMap

接下來看一下相應的源碼實現,首先來看一下內部定義的 ThreadLocalMap 靜態內部類:

static class ThreadLocalMap {

    // 弱引用的key,繼承自 WeakReference
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** ThreadLocal 修飾的對象 */
        Object value;

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

    /** 初始化大小,必須是二次冪 */
    private static final int INITIAL_CAPACITY = 16;
    /** 承載鍵值對的表,長度必須是二次冪 */
    private Entry[] table;
    /** 記錄鍵值對錶的大小 */
    private int size = 0;
    /** 再散列閾值 */
    private int threshold; // Default to 0

    // 構造方法
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }

    // 構造方法
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];

        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }
      private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

    // 省略相應的方法實現
}

ThreadLocalMap 是一個定製化的 Map 實現,這裏可以簡單將其理解爲一般的 Map,用作鍵值存儲的內存數據庫,至於爲什麼要專門實現而不是複用已有的 HashMap,我們在後面進行說明。

1.2 ThreadLocal 方法實現

瞭解了 ThreadLocalMap 的定義,我們再來看一下 ThreadLocal 的實現。對於 ThreadLocal 來說,對外暴露的方法主要有 get、set,以及 remove 三個,下面逐一來看:

  • 獲取線程私有值:get()

與一般的 Map 取值操作不同,這裏的 get() 並沒有要求提供查詢的 key,也正如前面所說的,這裏的 key 就是調用 get()方法的對象自身:

public T get() {
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 獲取當前線程對象的 threadLocals 屬性
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以 ThreadLocal 對象爲 key 獲取目標線程私有值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

如果當前線程對應的內存數據庫 map 對象還未創建,則會調用 setInitialValue() 方法執行創建,如果在構造 ThreadLocal 對象時覆蓋實現了 initialValue() 方法,則會調用該方法獲取構造的初始化值並記錄到創建的 map 對象中:

private T setInitialValue() {
    // 調用模板方法 initialValue 獲取指定的初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 以當前 ThreadLocal 對象爲 key 記錄初始值
        map.set(this, value);
    else
        // 創建 map 並記錄初始值
        createMap(t, value);
    return value;
}
  • 添加線程私有值:set(T value)

再來看一下 set 方法,因爲 key 就是當前 ThreadLocal 對象,所以 set 方法也不需要指定 key:

public void set(T value) {
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 獲取當前線程對象的 threadLocals 屬性
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 以當前 ThreadLocal 對象爲 key 記錄線程私有值
        map.set(this, value);
    else
        createMap(t, value);
}

和 get 方法的流程大致一樣,都是操作當前線程私有的內存數據庫 ThreadLocalMap,並記錄目標值。

  • 刪除線程私有值:remove()

remove 方法以當前 ThreadLocal 爲 key,從當前線程內存數據庫 ThreadLocalMap 中刪除目標值,具體邏輯比較簡單:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 以當前 ThreadLocal 對象爲 key
        m.remove(this);
}

ThreadLocal 對外暴露的功能雖然有點小神奇,但是具體對應到內部實現並沒有什麼複雜的邏輯,如果我們把每個線程持有的專屬 ThreadLocalMap 對象理解爲當前線程的私有數據庫,那麼也就不難理解 ThreadLocal 的運行機制,每個線程自己維護自己的數據,彼此相互隔離,不存在競爭,也就沒有線程安全問題可言。

二. 真的就高枕無憂了嗎?

雖然對於每個線程來說數據是隔離的,但這也不表示任何對象丟到 ThreadLocal 中就萬事大吉了,思考一下下面幾種情況:

  1. 如果記錄在 ThreadLocal 中的是一個線程共享的外部對象呢?
  2. 引入線程池,情況又會有什麼變化?
  3. 如果 ThreadLocal 被 static 關鍵字修飾呢?

先來看 第一個問題 ,如果我們記錄的是一個外部線程共享的對象,雖然我們以當前線程私有的 ThreadLocal 對象作爲 key 對其進行了存儲,但是惡魔終究是惡魔,共享的本質並不會因此而改變,這種情況下的訪問還是需要進行同步控制,最好的方法就是從源頭屏蔽掉這類問題。我們來舉個例子:

public class ThreadLocalWithSharedInstance implements Runnable {

    // list 是一個事實共享的實例,即使被 ThreadLocal 修飾
    private static List<String> list = new ArrayList<>();
    private ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> list);

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            List<String> li = threadLocal.get();
            li.add(Thread.currentThread().getName() + "_" + RandomUtils.nextInt(0, 10));
            threadLocal.set(li);
        }
        System.out.println("[Thread-" + Thread.currentThread().getName() + "], list=" + threadLocal.get());
    }

    public static void main(String[] args) throws Exception {
        Thread ta = new Thread(new ThreadLocalWithSharedInstance(), "a");
        Thread tb = new Thread(new ThreadLocalWithSharedInstance(), "b");
        Thread tc = new Thread(new ThreadLocalWithSharedInstance(), "c");
        ta.start(); ta.join();
        tb.start(); tb.join();
        tc.start(); tc.join();
    }
}

以上程序最終的輸出如下:

[Thread-a], list=[a_2, a_7, a_4, a_5, a_7]
[Thread-b], list=[a_2, a_7, a_4, a_5, a_7, b_3, b_3, b_4, b_7, b_7]
[Thread-c], list=[a_2, a_7, a_4, a_5, a_7, b_3, b_3, b_4, b_7, b_7, c_8, c_3, c_4, c_7, c_5]

可以看到雖然使用了 ThreadLocal 修飾,但是 list 還是以共享的方式在多個線程之間被訪問,如果不加同步控制,則會存在線程安全問題。

再來看 第二個問題 ,相對問題一來說引入線程池就更加可怕,因爲大部分時候我們都不會意識到問題的存在,直到代碼暴露出奇怪的現象,這個時候並沒有違背線程私有的本質,只是一個線程被複用來處理多個業務,而這個被線程私有的對象也會在多個業務之間被 “共享”。例如:

public class ThreadLocalWithThreadPool implements Callable<Boolean> {

    private static final int NCPU = Runtime.getRuntime().availableProcessors();

    private ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> {
        System.out.println("thread-" + Thread.currentThread().getId() + " init thread local");
        return new ArrayList<>();
    });

    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 5; i++) {
            List<String> li = threadLocal.get();
            li.add(Thread.currentThread().getId() + "_" + RandomUtils.nextInt(0, 10));
            threadLocal.set(li);
        }
        System.out.println("[Thread-" + Thread.currentThread().getId() + "], list=" + threadLocal.get());
        return true;
    }

    public static void main(String[] args) throws Exception {
        System.out.println("cpu core size : " + NCPU);
        List<Callable<Boolean>> tasks = new ArrayList<>(NCPU * 2);
        ThreadLocalWithThreadPool tl = new ThreadLocalWithThreadPool();
        for (int i = 0; i < NCPU * 2; i++) {
            tasks.add(tl);
        }
        ExecutorService es = Executors.newFixedThreadPool(2);
        List<Future<Boolean>> futures = es.invokeAll(tasks);
        for (final Future<Boolean> future : futures) {
            future.get();
        }
        es.shutdown();
    }
}

以上程序的最終輸出如下:

cpu core size : 8
thread-12 init thread local
thread-11 init thread local
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9, 12_5, 12_7, 12_7, 12_9, 12_7]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2, 11_4, 11_9, 11_7, 11_5, 11_5]
[Thread-12], list=[12_8, 12_8, 12_4, 12_0, 12_1, 12_6, 12_7, 12_8, 12_8, 12_8, 12_8, 12_2, 12_8, 12_0, 12_6, 12_6, 12_3, 12_3, 12_1, 12_1, 12_0, 12_0, 12_1, 12_9, 12_5, 12_3, 12_6, 12_6, 12_0, 12_9, 12_5, 12_7, 12_7, 12_9, 12_7, 12_6, 12_1, 12_7, 12_8, 12_7]
[Thread-11], list=[11_3, 11_3, 11_4, 11_8, 11_4, 11_0, 11_2, 11_1, 11_7, 11_9, 11_0, 11_6, 11_1, 11_2, 11_9, 11_7, 11_5, 11_0, 11_6, 11_9, 11_2, 11_7, 11_0, 11_8, 11_0, 11_0, 11_9, 11_2, 11_7, 11_2, 11_4, 11_9, 11_7, 11_5, 11_5, 11_8, 11_5, 11_0, 11_2, 11_2]

在我的 8 核處理器上,我用一個大小爲 2 的線程池進行了模擬,可以看到初始化方法被調用了兩次,所有線程的操作都是複用這兩個線程。回憶一下前文所說的,ThreadLocal 的本質就是每個線程維護一個線程私有的內存數據庫來記錄線程私有的對象,但是在線程池情況下線程是會被複用的,也就是說線程私有的內存數據庫也會被複用,如果在一個線程被使用完準備回放到線程池中之前,我們沒有對記錄在數據庫中的數據執行清理,那麼這部分數據就會被下一個複用該線程的業務看到,從而間接的共享了該部分數據(哈哈,你的筆記本電腦在送人之前一定要對硬盤執行多次格式化,不然冠希哥會對你微笑哦)。

最後我們再來看一下 第三個問題 ,我們嘗試將 ThreadLocal 對象用 static 關鍵字進行修飾:

public class ThreadLocalWithStaticEmbellish implements Runnable {

    private static final int NCPU = Runtime.getRuntime().availableProcessors();

    private static ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(() -> {
        System.out.println("thread-" + Thread.currentThread().getName() + " init thread local");
        return new ArrayList<>();
    });

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            List<String> li = threadLocal.get();
            li.add(Thread.currentThread().getId() + "_" + RandomUtils.nextInt(0, 10));
            threadLocal.set(li);
        }
        System.out.println("[Thread-" + Thread.currentThread().getName() + "], list=" + threadLocal.get());
    }

    public static void main(String[] args) throws Exception {
        ThreadLocalWithStaticEmbellish tl = new ThreadLocalWithStaticEmbellish();
        for (int i = 0; i < NCPU + 1; i++) {
            Thread thread = new Thread(tl, String.valueOf((char) (i + 97)));
            thread.start(); thread.join();
        }
    }
}

以上程序的最終輸出如下:

thread-a init thread local
[Thread-a], list=[11_4, 11_4, 11_4, 11_8, 11_0]
thread-b init thread local
[Thread-b], list=[12_0, 12_9, 12_0, 12_3, 12_3]
thread-c init thread local
[Thread-c], list=[13_6, 13_7, 13_5, 13_2, 13_0]
thread-d init thread local
[Thread-d], list=[14_1, 14_5, 14_5, 14_9, 14_2]
thread-e init thread local
[Thread-e], list=[15_4, 15_2, 15_6, 15_0, 15_8]
thread-f init thread local
[Thread-f], list=[16_7, 16_3, 16_8, 16_0, 16_0]
thread-g init thread local
[Thread-g], list=[17_6, 17_3, 17_8, 17_7, 17_1]
thread-h init thread local
[Thread-h], list=[18_0, 18_4, 18_5, 18_9, 18_3]
thread-i init thread local
[Thread-i], list=[19_7, 19_3, 19_7, 19_2, 19_0]

由程序運行結果可以看到 static 修飾並沒有引出什麼問題,實際上這也是很容易理解的,ThreadLocal 採用 static 修飾僅僅是讓數據庫中記錄的 key 是一樣的,但是每個線程的內存數據庫還是私有的,並沒有被共享,就像不同的公司都有自己的用戶信息表,即使一些公司之間的用戶 ID 是一樣的,但是對應的用戶數據卻是完全隔離的。

以上例子演示了一開始拋出的三個問題,其中問題一和問題二都是 ThreadLocal 使用過程中的小地雷。例子舉的不一定恰當,實際中可能也不一定會如示例中這樣去使用 ThreadLocal,主要還是爲了傳達一些意識。如果明白了 ThreadLocal 的內部實現細節,就能夠很自然的繞過這些小地雷。

三. 真的會內存泄露嗎?

關於 ThreadLocal 導致內存泄露的問題,曾經有一段時間在網上爭得沸沸揚揚,那麼到底會不會導致內存泄露呢?這裏先給出答案:

如果使用不恰當,存在內存泄露的可能性。

我們來分析一下內存泄露的條件和原因,在最開始看 ThreadLocal 源碼的時候,我就有一個疑問,__ThreadLocal 爲什麼要專門實現 ThreadLocalMap,而不是採用已有的 HashMap 代替__?後來分析具體實現時看到執行存儲時的 key 爲當前 ThreadLocal 對象,不需要專門指定 key 能夠在一定程度上簡化使用,但這並不足以爲此專門去實現 ThreadLocalMap。繼續閱讀我發現 ThreadLocalMap 在實現 Entry 的時候有些奇怪,居然繼承了 WeakReference:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

從而讓 key 成爲一個弱引用,我們知道弱引用對象擁有非常短暫的生命週期,在垃圾收集器線程掃描其所管轄的內存區域過程中,一旦發現了弱引用對象,不管當前內存空間是否足夠都會回收它的內存。也就是說這樣的設計會很容易導致 ThreadLocal 對象被回收,線程所執行任務的時間長度是不固定的,這樣的設計能夠方便垃圾收集器回收線程私有的變量。

所以作者這樣設計的目的是爲了防止內存泄露,那怎麼就變成了被很多文章所分析的是內存泄漏的導火索呢?這些文章的共同觀點就是 key 被回收了,但是 value 是一個強引用沒有被回收,這些 value 就變成了一個個的殭屍。這樣的分析沒有錯,value 確實存在,且和線程是同生命週期的,但是如下策略可以保證儘量避免內存泄露:

  1. ThreadLocal 在每次執行 get 和 set 操作的時候都會去清理 key 爲 null 的 value 值
  2. value 與線程同生命週期,線程死亡之時,也是 value 被 GC 之日

策略一沒啥好說的,看看源碼就知道,我們來舉例驗證一下策略二:

public class ThreadLocalWithMemoryLeak implements Callable<Boolean> {

    private class My50MB {

        private byte[] buffer = new byte[50 * 1024 * 1024];

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("gc my 50 mb");
        }
    }

    private class MyThreadLocal<T> extends ThreadLocal<T> {

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("gc my thread local");
        }
    }

    private MyThreadLocal<My50MB> threadLocal = new MyThreadLocal<>();

    @Override
    public Boolean call() throws Exception {
        System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
        threadLocal.set(new My50MB());
        threadLocal = null;
        return true;
    }

    public static void main(String[] args) throws Exception {
        ExecutorService es = Executors.newCachedThreadPool();
        Future<Boolean> future = es.submit(new ThreadLocalWithMemoryLeak());
        future.get();

        // gc my thread local
        System.out.println("do gc");
        System.gc();
        TimeUnit.SECONDS.sleep(1);

        // sleep 60s
        System.out.println("sleep 60s");
        TimeUnit.SECONDS.sleep(60);

        // gc my 50 mb
        System.out.println("do gc");
        System.gc();

        es.shutdown();
    }

}

以上程序的最終輸出如下:

Thread-11 is running
do gc
gc my thread local
sleep 60s
do gc
gc my 50 mb

可以看到 value 最終還是被 GC 了,雖然第一次 GC 的時候沒有被回收,這也驗證 value 和線程是同生命週期的,之所以示例中等待 60 秒是因爲 Executors.newCachedThreadPool() 中的線程默認生命週期是 60 秒,如果生命週期內該線程沒有被再次複用則會死亡,我們這裏就是要等待線程死亡,一但線程死亡,value 也就被 GC 了。所以 出現內存泄露的前提必須是持有 value 的線程一直存活 ,這在使用線程池時是很正常的,在這種情況下 value 一直不會被 GC,因爲線程對象與 value 之間維護的是強引用。此外就是 後續線程執行的業務一直沒有調用 ThreadLocal 的 get 或 set 方法,導致不會主動去刪除 key 爲 null 的 value 對象 ,在滿足這兩個條件下 value 對象一直常駐內存,所以存在內存泄露的可能性。

那麼我們應該怎麼避免呢?前面我們分析過線程池情況下使用 ThreadLocal 存在小地雷,這裏的內存泄露一般也都是發生在線程池的情況下,所以在使用 ThreadLocal 時,對於不再有效的 value 主動調用一下 remove 方法來進行清除,從而消除隱患,這也算是最佳實踐吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章