ThreadLocal 線程安全機制與小地雷

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

下面的例子演示了 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 靜態內部類:bash

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++;
                }
            }
        }
    }

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

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 對象中:dom

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:ide

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,並記錄目標值。this

  • 刪除線程私有值: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 方法來進行清除,從而消除隱患,這也算是最佳實踐吧。


本文最早發佈於 「 指間數據 」 公衆號,微信掃描下方二維碼進行關注,第一時間獲取高質量的技術類文章。

微信掃描二維碼關注指間數據公衆號

相關文章
相關標籤/搜索