溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制

發(fā)布時(shí)間:2021-11-15 15:50:46 來(lái)源:億速云 閱讀:133 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容主要講解“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”吧!

下面的例子演示了 ThreadLocal 的典型應(yīng)用場(chǎng)景。在 jdk 1.8 之前,如果我們希望對(duì)日期和時(shí)間進(jìn)行格式化操作,則需要使用 SimpleDateFormat 類,而我們知道它是是線程不安全的,在多線程并發(fā)執(zhí)行時(shí)會(huì)出現(xiàn)一些奇怪的問(wèn)題。對(duì)于該類使用的最佳實(shí)踐則是采用 ThreadLocal 進(jìn)行包裝,以保證每個(gè)線程都有一份屬于自己的 SimpleDateFormat 對(duì)象,如下所示:

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

實(shí)現(xiàn)機(jī)制

那么 ThreadLocal 是怎么做到讓修飾的對(duì)象能夠在每個(gè)線程中各自持有一份呢?我們先來(lái)從整體的角度簡(jiǎn)單概括一下。

在 ThreadLocal 中定義了一個(gè)靜態(tài)內(nèi)部類 ThreadLocalMap,可以將其理解為一個(gè)特有的 Map 類型,而在 Thread 類中聲明了一個(gè) ThreadLocalMap 類型的 threadLocals 屬性。針對(duì)每個(gè) Thread 對(duì)象,也就是每個(gè)線程來(lái)說(shuō)都包含了一個(gè) ThreadLocalMap 對(duì)象,即每個(gè)線程都有一個(gè)屬于自己的內(nèi)存數(shù)據(jù)庫(kù),而數(shù)據(jù)庫(kù)中存儲(chǔ)的就是我們用 ThreadLocal 修飾的對(duì)象。整個(gè)過(guò)程還是有點(diǎn)繞的,可以借助下面這幅圖進(jìn)行理解:

怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制

這里的 key 就是對(duì)應(yīng)的 ThreadLocal 對(duì)象自身,而 value 就是 ThreadLocal 修飾的屬性值。當(dāng)希望獲取該對(duì)象時(shí),我們首先需要拿到當(dāng)前線程對(duì)應(yīng)的 Thread 對(duì)象,然后獲取到該對(duì)象對(duì)應(yīng)的 threadLocals 屬性,也就拿到了線程私有的內(nèi)存數(shù)據(jù)庫(kù),最后以 ThreadLocal 對(duì)象為 key 獲取到其修飾的目標(biāo)值。

線程內(nèi)存數(shù)據(jù)庫(kù)

接下來(lái)看一下相應(yīng)的源碼實(shí)現(xiàn),首先來(lái)看一下內(nèi)部定義的 ThreadLocalMap 靜態(tài)內(nèi)部類:

static class ThreadLocalMap {

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

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

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

    // 構(gòu)造方法
    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);
    }

    // 構(gòu)造方法
    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++;
                }
            }
        }
    }

    // 省略相應(yīng)的方法實(shí)現(xiàn)
}

ThreadLocalMap 是一個(gè)定制化的 Map 實(shí)現(xiàn),可以簡(jiǎn)單將其理解為一般的 Map,用作鍵值存儲(chǔ)的內(nèi)存數(shù)據(jù)庫(kù),至于為什么要專門實(shí)現(xiàn)而不是復(fù)用已有的 HashMap,我們?cè)诤竺孢M(jìn)行說(shuō)明。

API 實(shí)現(xiàn)分析

了解了 ThreadLocalMap 的定義,我們?cè)賮?lái)看一下 ThreadLocal 的實(shí)現(xiàn)。對(duì)于 ThreadLocal 來(lái)說(shuō),對(duì)外暴露的方法主要有 get、set,以及 remove 三個(gè),下面逐一展開(kāi)分析。

獲取線程私有值

與一般的 Map 取值操作不同,這里的 ThreadLocal#get 方法并沒(méi)有要求提供查詢的 key,也正如前面所說(shuō)的,這里的 key 就是調(diào)用 ThreadLocal#get 方法的 ThreadLocal 對(duì)象自身:

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

如果當(dāng)前線程對(duì)應(yīng)的內(nèi)存數(shù)據(jù)庫(kù) map 對(duì)象還未創(chuàng)建,則會(huì)調(diào)用 ThreadLocal#setInitialValue 方法執(zhí)行創(chuàng)建,如果在構(gòu)造 ThreadLocal 對(duì)象時(shí)覆蓋實(shí)現(xiàn)了 ThreadLocal#initialValue 方法,則會(huì)調(diào)用該方法獲取構(gòu)造的初始化值并記錄到創(chuàng)建的 map 對(duì)象中:

private T setInitialValue() {
    // 調(diào)用模板方法 initialValue 獲取指定的初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 以當(dāng)前 ThreadLocal 對(duì)象為 key 記錄初始值
        map.set(this, value);
    else
        // 創(chuàng)建 map 并記錄初始值
        createMap(t, value);
    return value;
}
設(shè)置線程私有值

再來(lái)看一下 ThreadLocal#set 方法,因?yàn)?key 就是當(dāng)前 ThreadLocal 對(duì)象,所以 ThreadLocal#set 方法也不需要指定 key:

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

ThreadLocal#get 方法的流程大致一樣,都是操作當(dāng)前線程私有的內(nèi)存數(shù)據(jù)庫(kù) ThreadLocalMap,并記錄目標(biāo)值。

刪除線程私有值

方法 ThreadLocal#remove 以當(dāng)前 ThreadLocal 對(duì)象為 key,從當(dāng)前線程內(nèi)存數(shù)據(jù)庫(kù) ThreadLocalMap 中刪除目標(biāo)值,具體邏輯比較簡(jiǎn)單:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 以當(dāng)前 ThreadLocal 對(duì)象為 key
        m.remove(this);
}

ThreadLocal 對(duì)外暴露的功能雖然有點(diǎn)小神奇,但是具體對(duì)應(yīng)到內(nèi)部實(shí)現(xiàn)并沒(méi)有什么復(fù)雜的邏輯。如果我們把每個(gè)線程持有的專屬 ThreadLocalMap 對(duì)象理解為當(dāng)前線程的私有數(shù)據(jù)庫(kù),那么也就不難理解 ThreadLocal 的運(yùn)行機(jī)制。每個(gè)線程自己維護(hù)自己的數(shù)據(jù),彼此相互隔離,不存在競(jìng)爭(zhēng),也就沒(méi)有線程安全問(wèn)題可言。

真的就高枕無(wú)憂了嗎

雖然對(duì)于每個(gè)線程來(lái)說(shuō)數(shù)據(jù)是隔離的,但這也不表示任何對(duì)象丟到 ThreadLocal 中就萬(wàn)事大吉了,思考一下下面幾種情況:

  1. 如果記錄在 ThreadLocal 中的是一個(gè)線程共享的外部對(duì)象呢?

  2. 引入線程池,情況又會(huì)有什么變化?

  3. 如果 ThreadLocal 被 static 關(guān)鍵字修飾呢?

先來(lái)看 第 1 個(gè)問(wèn)題,如果我們記錄的是一個(gè)外部線程共享的對(duì)象,雖然我們以當(dāng)前線程私有的 ThreadLocal 對(duì)象作為 key 對(duì)其進(jìn)行了存儲(chǔ),但是惡魔終究是惡魔,共享的本質(zhì)并不會(huì)因此而改變,這種情況下的訪問(wèn)還是需要進(jìn)行同步控制,最好的方法就是從源頭屏蔽掉這類問(wèn)題。我們來(lái)舉個(gè)例子:

public class ThreadLocalWithSharedInstance implements Runnable {

    // list 是一個(gè)事實(shí)共享的實(shí)例,即使被 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 還是以共享的方式在多個(gè)線程之間被訪問(wèn),如果不加控制則會(huì)存在線程安全問(wèn)題。

再來(lái)看 第 2 個(gè)問(wèn)題,相對(duì)問(wèn)題 1 來(lái)說(shuō)引入線程池就更加可怕,因?yàn)榇蟛糠謺r(shí)候我們都不會(huì)意識(shí)到問(wèn)題的存在,直到代碼暴露出奇怪的現(xiàn)象。這一場(chǎng)景并沒(méi)有違背線程私有的本質(zhì),只是一個(gè)線程被復(fù)用來(lái)處理多個(gè)業(yè)務(wù),而這個(gè)被線程私有的對(duì)象也會(huì)在多個(gè)業(yè)務(wù)之間被共享。例如:

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]

示例中,我用一個(gè)大小為 2 的線程池進(jìn)行了模擬,可以看到初始化方法被調(diào)用了兩次,所有線程的操作都是復(fù)用這兩個(gè)線程。

回憶一下前文所說(shuō)的,ThreadLocal 的本質(zhì)就是為每個(gè)線程維護(hù)一個(gè)線程私有的內(nèi)存數(shù)據(jù)庫(kù)來(lái)記錄線程私有的對(duì)象,但是在線程池情況下線程是會(huì)被復(fù)用的,也就是說(shuō)線程私有的內(nèi)存數(shù)據(jù)庫(kù)也會(huì)被復(fù)用,如果在一個(gè)線程被使用完準(zhǔn)備回放到線程池中之前,我們沒(méi)有對(duì)記錄在數(shù)據(jù)庫(kù)中的數(shù)據(jù)執(zhí)行清理,那么這部分?jǐn)?shù)據(jù)就會(huì)被下一個(gè)復(fù)用該線程的業(yè)務(wù)看到,從而間接的共享了該部分?jǐn)?shù)據(jù)。

最后我們?cè)賮?lái)看一下 第 3 個(gè)問(wèn)題,我們嘗試將 ThreadLocal 對(duì)象用 static 關(guān)鍵字進(jìn)行修飾:

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]

由程序運(yùn)行結(jié)果可以看到 static 修飾并沒(méi)有引出什么問(wèn)題,實(shí)際上這也是很容易理解的,ThreadLocal 采用 static 修飾僅僅是讓數(shù)據(jù)庫(kù)中記錄的 key 是一樣的,但是每個(gè)線程的內(nèi)存數(shù)據(jù)庫(kù)還是私有的,并沒(méi)有被共享,就像不同的公司都有自己的用戶信息表,即使一些公司之間的用戶 ID 是一樣的,但是對(duì)應(yīng)的用戶數(shù)據(jù)卻是完全隔離的。

以上例子演示了一開(kāi)始拋出的 3 個(gè)問(wèn)題,其中問(wèn)題 1 和問(wèn)題 2 都是 ThreadLocal 使用過(guò)程中的小地雷。例子舉的不一定恰當(dāng),實(shí)際中可能也不一定會(huì)如示例中這樣去使用 ThreadLocal,主要還是為了傳達(dá)一些意識(shí)。如果明白了 ThreadLocal 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),就能夠很自然的繞過(guò)這些小地雷。

真的會(huì)內(nèi)存泄露嗎

關(guān)于 ThreadLocal 導(dǎo)致內(nèi)存泄露的問(wèn)題,曾經(jīng)有一段時(shí)間在網(wǎng)上爭(zhēng)得沸沸揚(yáng)揚(yáng),那么到底會(huì)不會(huì)導(dǎo)致內(nèi)存泄露呢?這里先給出答案:

如果使用不恰當(dāng),存在內(nèi)存泄露的可能性。

我們來(lái)分析一下內(nèi)存泄露的條件和原因,在最開(kāi)始看 ThreadLocal 源碼的時(shí)候,我就有一個(gè)疑問(wèn),ThreadLocal 為什么要專門實(shí)現(xiàn) ThreadLocalMap,而不是采用已有的 HashMap 代替?

后來(lái)分析具體實(shí)現(xiàn)時(shí)看到執(zhí)行存儲(chǔ)時(shí)的 key 為當(dāng)前 ThreadLocal 對(duì)象,不需要專門指定 key 能夠在一定程度上簡(jiǎn)化使用,但這并不足以為此專門去實(shí)現(xiàn) ThreadLocalMap。繼續(xù)閱讀我發(fā)現(xiàn) ThreadLocalMap 在實(shí)現(xiàn) Entry 的時(shí)候有些奇怪,居然繼承了 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 成為一個(gè)弱引用,我們知道弱引用對(duì)象擁有非常短暫的生命周期,在垃圾收集器線程掃描其所管轄的內(nèi)存區(qū)域過(guò)程中,一旦發(fā)現(xiàn)了弱引用對(duì)象,不管當(dāng)前內(nèi)存空間是否足夠都會(huì)回收它的內(nèi)存。也就是說(shuō)這樣的設(shè)計(jì)會(huì)很容易導(dǎo)致 ThreadLocal 對(duì)象被回收,線程所執(zhí)行任務(wù)的時(shí)間長(zhǎng)度是不固定的,這樣的設(shè)計(jì)能夠方便垃圾收集器回收線程私有的變量。

由此可以看出作者這樣設(shè)計(jì)的目的是為了防止內(nèi)存泄露,那怎么就變成了被很多文章所分析的是內(nèi)存泄漏的導(dǎo)火索呢?這些文章的共同觀點(diǎn)就是 key 被回收了,但是 value 是一個(gè)強(qiáng)引用沒(méi)有被回收,這些 value 就變成了一個(gè)個(gè)的僵尸。這樣的分析沒(méi)有錯(cuò),value 確實(shí)存在,且和線程是同生命周期的,但是如下策略可以保證盡量避免內(nèi)存泄露:

  1. ThreadLocal 在每次執(zhí)行 get 和 set 操作的時(shí)候都會(huì)去清理 key 為 null 的 value 值。

  2. value 與線程同生命周期,線程死亡之時(shí),也是 value 被 GC 之日。

策略 1 沒(méi)啥好說(shuō)的,看看源碼就知道,我們來(lái)舉例驗(yàn)證一下策略 2:

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 了,雖然第 1 次 GC 的時(shí)候沒(méi)有被回收,這也驗(yàn)證 value 和線程是同生命周期的,之所以示例中等待 60 秒是因?yàn)?Executors#newCachedThreadPool 中的線程默認(rèn)生命周期是 60 秒,如果生命周期內(nèi)該線程沒(méi)有被再次復(fù)用則會(huì)死亡,我們這里就是要等待線程死亡,一但線程死亡,value 也就被 GC 了。

所以 出現(xiàn)內(nèi)存泄露的前提必須是持有 value 的線程一直存活,這在使用線程池時(shí)是很正常的,在這種情況下 value 一直不會(huì)被 GC,因?yàn)榫€程對(duì)象與 value 之間維護(hù)的是強(qiáng)引用。此外就是 后續(xù)線程執(zhí)行的業(yè)務(wù)一直沒(méi)有調(diào)用 ThreadLocal 的 get 或 set 方法,導(dǎo)致不會(huì)主動(dòng)去刪除 key 為 null 的 value 對(duì)象,在滿足這兩個(gè)條件下 value 對(duì)象一直常駐內(nèi)存,所以存在內(nèi)存泄露的可能性。

那么我們應(yīng)該怎么避免呢?前面我們分析過(guò)線程池情況下使用 ThreadLocal 存在小地雷,這里的內(nèi)存泄露一般也都是發(fā)生在線程池的情況下,所以在使用 ThreadLocal 時(shí),對(duì)于不再有效的 value 主動(dòng)調(diào)用一下 remove 方法來(lái)進(jìn)行清除,從而消除隱患,這也算是最佳實(shí)踐吧。

InheritableThreadLocal 又是什么鬼

InheritableThreadLocal 繼承自 ThreadLocal,實(shí)現(xiàn)上也比較簡(jiǎn)單(如下),那么 InheritableThreadLocal 與 ThreadLocal 到底有什么區(qū)別呢?

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    @Override
    protected T childValue(T parentValue) {
        return parentValue;
    }

    @Override
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }

    @Override
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在開(kāi)始分析之前,我們先演示一個(gè) ThreadLocal 的案例,如下:

private static ThreadLocal<String> tl = new ThreadLocal<>();

public static void main(String[] args) {
    tl.set("zhenchao");
    System.out.println("Main thread: " + tl.get());
    Thread thread = new Thread(() -> System.out.println("Sub thread: " + tl.get()));
    thread.start();
}

運(yùn)行上述示例,輸出如下:

Main thread: zhenchao
Sub thread: null

可以看出,子線程拿不到主線程設(shè)置的 ThreadLocal 變量,當(dāng)然這也是可以理解的,畢竟主線程和子線程之間仍然是兩個(gè)線程,但是在一些場(chǎng)景下我們希望對(duì)于主線程和子線程這種關(guān)系而言,ThreadLocal 變量能夠被繼承。這個(gè)時(shí)候就可以使用 InheritableThreadLocal 來(lái)實(shí)現(xiàn),對(duì)于上述示例而言,只需要將 ThreadLocal 改為 InheritableThreadLocal 即可,具體實(shí)現(xiàn)比較簡(jiǎn)單,讀者可以自己嘗試一下。

下面我們來(lái)分析一下 InheritableThreadLocal 是如何做到讓 ThreadLocal 變量在主線程和子線程之間進(jìn)行繼承的。由 InheritableThreadLocal 的實(shí)現(xiàn)來(lái)看,InheritableThreadLocal 使用了 inheritableThreadLocals 變量替換了 ThreadLocal 的 threadLocals 變量,而這兩個(gè)變量都是 ThreadLocalMap 類型。子線程在初始化時(shí)會(huì)判斷父線程的 inheritableThreadLocals 是否為 null,如果不為 null,則使用父類的 inheritableThreadLocals 變量初始化自己的 inheritableThreadLocals,實(shí)現(xiàn)如下(位于 Thread#init 方法中):

// 如果父線程的 inheritableThreadLocals 變量不為空,則復(fù)制給子線程
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

ThreadLocal#createInheritedMap 的實(shí)現(xiàn)如下:

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

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) {
                // 調(diào)用 InheritableThreadLocal 的 childValue 方法
                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++;
            }
        }
    }
}

方法 InheritableThreadLocal#childValue 的實(shí)現(xiàn)只是簡(jiǎn)單返回了父線程中的值,所以上述過(guò)程本質(zhì)上就是一個(gè)拷貝父線程中 ThreadLocal 變量值的過(guò)程。

到此,相信大家對(duì)“怎么理解ThreadLocal的實(shí)現(xiàn)機(jī)制”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI