自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

阿里二面:談?wù)凾hreadLocal的內(nèi)存泄漏問(wèn)題?

開(kāi)發(fā) 前端
本文探討了ThreadLocal?的工作原理以及其內(nèi)存泄漏問(wèn)題及解決策略。ThreadLocal?通過(guò)為每個(gè)線程提供獨(dú)立的變量副本,實(shí)現(xiàn)多線程環(huán)境下的數(shù)據(jù)隔離。

引言

ThreadLocal在Java多線程編程中扮演著重要的角色,它提供了一種線程局部存儲(chǔ)機(jī)制,允許每個(gè)線程擁有獨(dú)立的變量副本,從而有效地避免了線程間的數(shù)據(jù)共享沖突。ThreadLocal的主要用途在于,當(dāng)需要為每個(gè)線程維護(hù)一個(gè)獨(dú)立的上下文變量時(shí),比如每個(gè)線程的事務(wù)ID、用戶登錄信息、數(shù)據(jù)庫(kù)連接等,可以減少對(duì)同步機(jī)制如synchronized關(guān)鍵字或Lock類(lèi)的依賴(lài),提高系統(tǒng)的執(zhí)行效率和簡(jiǎn)化代碼邏輯。

但是我們?cè)谑褂肨hreadLocal時(shí),經(jīng)常因?yàn)槭褂貌划?dāng)導(dǎo)致內(nèi)存泄漏。此時(shí)就需要我們?nèi)ヌ骄恳幌耇hreadLocal在哪些場(chǎng)景下會(huì)出現(xiàn)內(nèi)存泄露?哪些場(chǎng)景下不會(huì)出現(xiàn)內(nèi)存泄露?出現(xiàn)內(nèi)存泄露的根本原因又是什么呢?如何避免內(nèi)存泄露?

ThreadLocal原理

ThreadLocal的實(shí)現(xiàn)基于每個(gè)線程內(nèi)部維護(hù)的一個(gè)ThreadLocalMap。

public class Thread implements Runnable {
     /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap是ThreadLocal類(lèi)的一個(gè)靜態(tài)內(nèi)部類(lèi),ThreadLocal本身不能存儲(chǔ)數(shù)據(jù),它在作用上更像一個(gè)工具類(lèi),ThreadLocal類(lèi)提供了set(T value)、get()等方法來(lái)操作ThreadLocalMap存儲(chǔ)數(shù)據(jù)。

public class ThreadLocal<T> {
    // ...
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // ...
}

而ThreadLocalMap內(nèi)部維護(hù)了一個(gè)Entry數(shù)據(jù),用來(lái)存儲(chǔ)數(shù)據(jù),Entry繼承了WeakReference,所以Entry的key是一個(gè)弱引用,可以被GC回收。Entry數(shù)組中的每一個(gè)元素都是一個(gè)Entry對(duì)象。每個(gè)Entry對(duì)象中存儲(chǔ)著一個(gè)ThreadLocal對(duì)象與其對(duì)應(yīng)的value值。

static class ThreadLocalMap {

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

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

關(guān)于弱引用的知識(shí)點(diǎn),請(qǐng)參考:

而Entry數(shù)組中Entry對(duì)象的下標(biāo)位置是通過(guò)ThreadLocal的threadLocalHashCode計(jì)算出來(lái)的。

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (Entry e : parentTable) {
        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);
                // 通過(guò)key的threadLocalHashCode計(jì)算下標(biāo),這個(gè)key就是ThreadLocall對(duì)象
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

而從Entry數(shù)組中獲取對(duì)應(yīng)key即ThreadLocal對(duì)應(yīng)的value值時(shí),也是通過(guò)key的threadLocalHashCode計(jì)算下標(biāo),從而可以快速的返回對(duì)應(yīng)的Entry對(duì)象。

private Entry getEntry(ThreadLocal<?> key) {
// 通過(guò)key的threadLocalHashCode計(jì)算下標(biāo),這個(gè)key就是ThreadLocall對(duì)象
    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);
}

在Thread中,可以存儲(chǔ)多個(gè)ThreadLocal對(duì)象。Thread、ThreadLocal、ThreadLocalMap以及Entry數(shù)組的關(guān)系如下圖:

圖片圖片

ThreadLocal在哪些場(chǎng)景下不會(huì)出現(xiàn)內(nèi)存泄露?

當(dāng)一個(gè)對(duì)象失去所有強(qiáng)引用,或者它僅被弱引用、軟引用、虛引用關(guān)聯(lián)時(shí),垃圾收集器(GC)通常都能識(shí)別并回收這些對(duì)象,從而避免內(nèi)存泄漏的發(fā)生。當(dāng)我們?cè)谑謩?dòng)創(chuàng)建線程時(shí),若將變量存儲(chǔ)到ThreadLocal中,那么在Thread線程正常運(yùn)行的過(guò)程中,它會(huì)維持對(duì)內(nèi)部ThreadLocalMap實(shí)例的引用。只要該Thread線程持續(xù)執(zhí)行任務(wù),這種引用關(guān)系將持續(xù)存在,確保ThreadLocalMap實(shí)例及其中存儲(chǔ)的變量不會(huì)因無(wú)引用而被GC回收。

圖片圖片

當(dāng)線程執(zhí)行完任務(wù)并正常退出后,線程與內(nèi)部ThreadLocalMap實(shí)例之間的強(qiáng)引用關(guān)系隨之?dāng)嚅_(kāi),這意味著線程不再持有ThreadLocalMap的引用。在這種情況下,失去強(qiáng)引用的ThreadLocalMap對(duì)象將符合垃圾收集器(GC)的回收條件,進(jìn)而被自動(dòng)回收。與此同時(shí),鑒于ThreadLocalMap內(nèi)部的鍵(ThreadLocal對(duì)象)是弱引用,一旦ThreadLocalMap被回收,若此時(shí)沒(méi)有其他強(qiáng)引用指向這些ThreadLocal對(duì)象,它們也將被GC一并回收。因此,在線程結(jié)束其生命周期后,與之相關(guān)的ThreadLocalMap及其包含的ThreadLocal對(duì)象理論上都能夠被正確清理,避免了內(nèi)存泄漏問(wèn)題。

實(shí)際應(yīng)用中還需關(guān)注ThreadLocalMap中存儲(chǔ)的值(非鍵)是否為強(qiáng)引用類(lèi)型,因?yàn)榧幢沔I(ThreadLocal對(duì)象)被回收,如果值是強(qiáng)引用且沒(méi)有其他途徑釋放,仍可能導(dǎo)致內(nèi)存泄漏。

ThreadLocal在哪些場(chǎng)景下會(huì)出現(xiàn)內(nèi)存泄露?

在實(shí)際項(xiàng)目開(kāi)發(fā)中,如果為每個(gè)任務(wù)都手動(dòng)創(chuàng)建線程,這是一件很耗費(fèi)資源的方式,并且在阿里巴巴的開(kāi)發(fā)規(guī)范中也提到,不推薦使用手動(dòng)創(chuàng)建線程,推薦使用線程池來(lái)執(zhí)行相對(duì)應(yīng)的任務(wù)。那么當(dāng)我們使用線程池時(shí),線程池中的線程跟ThrealLocalMap的引用關(guān)系如下:

圖片圖片

在使用線程池處理任務(wù)時(shí),每一個(gè)線程都會(huì)關(guān)聯(lián)一個(gè)獨(dú)立的ThreadLocalMap對(duì)象,用于存儲(chǔ)線程本地變量。由于線程池中的核心線程在完成任務(wù)后不會(huì)被銷(xiāo)毀,而是保持活動(dòng)狀態(tài)等待接收新的任務(wù),這意味著核心線程與其內(nèi)部持有的ThreadLocalMap對(duì)象之間始終保持著強(qiáng)引用關(guān)系。因此,只要核心線程存活,其所對(duì)應(yīng)的ThreadLocal對(duì)象和ThreadLocalMap不會(huì)被垃圾收集器(GC)自動(dòng)回收,此時(shí)就會(huì)存在內(nèi)存泄露的風(fēng)險(xiǎn)。

出現(xiàn)內(nèi)存泄露的根本原因

由上述ThreadLocalMap的結(jié)構(gòu)圖以及ThreadLocalMap的源碼中,我們知道ThreadLocalMap中包含一個(gè)Entry數(shù)組,而Entry數(shù)組中的每一個(gè)元素就是Entry對(duì)象,Entry對(duì)象中存儲(chǔ)的Key就是ThreadLocal對(duì)象,而value就是要存儲(chǔ)的數(shù)據(jù)。其中,Entry對(duì)象中的Key屬于弱引用。

static class ThreadLocalMap {

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

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

而對(duì)于弱引用WeakReference,在引用的對(duì)象使用完畢之后,即使內(nèi)存足夠,GC也會(huì)對(duì)其進(jìn)行回收。

關(guān)于弱引用的知識(shí)點(diǎn),請(qǐng)參考:

圖片圖片

當(dāng)Entry對(duì)象中的Key被GC自動(dòng)回收后,對(duì)應(yīng)的ThreadLocal被GC回收掉了,變成了null,但是ThreadLocal對(duì)應(yīng)的value值依然被Entry引用,不能被GC自動(dòng)回收。這樣就造成了內(nèi)存泄漏的風(fēng)險(xiǎn)。

圖片圖片

在線程池環(huán)境下使用ThreadLocal存儲(chǔ)數(shù)據(jù)時(shí),內(nèi)存泄露的風(fēng)險(xiǎn)主要源自于線程生命周期管理及ThreadLocalMap內(nèi)部結(jié)構(gòu)的設(shè)計(jì)。由于線程池中的核心線程在完成任務(wù)后會(huì)復(fù)用,每個(gè)線程都會(huì)維持對(duì)各自關(guān)聯(lián)的ThreadLocalMap對(duì)象的強(qiáng)引用,這確保了只要線程持續(xù)存在,其對(duì)應(yīng)的ThreadLocalMap就無(wú)法被垃圾收集器(GC)自動(dòng)回收。

進(jìn)一步分析,ThreadLocalMap內(nèi)部采用一個(gè)Entry數(shù)組來(lái)保存鍵值對(duì),其中每個(gè)條目的Key是當(dāng)前線程中對(duì)應(yīng)ThreadLocal實(shí)例的弱引用,這意味著當(dāng)外部不再持有該ThreadLocal實(shí)例的強(qiáng)引用時(shí),Key部分能夠被GC正?;厥?。然而,關(guān)鍵在于Entry的Value部分,它直接或間接地持有著強(qiáng)引用的對(duì)象,即使Key因?yàn)槿跻锰匦员换厥?,但Value所引用的數(shù)據(jù)卻不會(huì)隨之釋放,除非明確移除或者整個(gè)ThreadLocalMap隨著線程結(jié)束而失效。

所以,在線程池中,如果未正確清理不再使用的ThreadLocal變量,其所持有的強(qiáng)引用數(shù)據(jù)將在多個(gè)任務(wù)執(zhí)行過(guò)程中逐漸積累并駐留在線程的ThreadLocalMap中,從而導(dǎo)致潛在的內(nèi)存泄露風(fēng)險(xiǎn)。

ThreadLocal如何避免內(nèi)存泄漏

經(jīng)過(guò)上述ThreadLocal原理以及發(fā)生內(nèi)存泄漏的分析,我們知道防止內(nèi)存泄漏,我們一定要在完成線程內(nèi)的任務(wù)后,調(diào)用ThreadLocal的remove()方法來(lái)清除當(dāng)前線程中ThreadLocal所對(duì)應(yīng)的值。其remove方法源碼如下:

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
         m.remove(this);
     }
 }

在remove()方法中,首先根據(jù)當(dāng)前線程獲取ThreadLocalMap類(lèi)型的對(duì)象,如果不為空,則直接調(diào)用該對(duì)象的有參remove()方法移除value的值。ThreadLocalMap的remove方法源碼如下:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

由上述ThreadLocalMap中的set()方法知道ThreadLocal中Entry下標(biāo)是通過(guò)計(jì)算ThreadLocal的hashCode獲得了,而remove()方法要找到需要移除value所在Entry數(shù)組中的下標(biāo)時(shí),也時(shí)通過(guò)當(dāng)前ThreadLocal對(duì)象的hashCode獲的,然后找到它的下標(biāo)之后,調(diào)用expungeStaleEntry將其value也置為null。我們繼續(xù)看一下expungeStaleEntry方法的源碼:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    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;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

在expungeStaleEntry()方法中,會(huì)將ThreadLocal為null對(duì)應(yīng)的value設(shè)置為null,同時(shí)會(huì)把對(duì)應(yīng)的Entry對(duì)象也設(shè)置為null,并且會(huì)將所有ThreadLocal對(duì)應(yīng)的value為null的Entry對(duì)象設(shè)置為null,這樣就去除了強(qiáng)引用,便于后續(xù)的GC進(jìn)行自動(dòng)垃圾回收,也就避免了內(nèi)存泄露的問(wèn)題。即調(diào)用完remove方法之后,ThreadLocalMap的結(jié)構(gòu)圖如下:

圖片圖片

在ThreadLocal中,不僅僅是remove()方法會(huì)調(diào)用expungeStaleEntry()方法,在set()方法和get()方法中也可能會(huì)調(diào)用expungeStaleEntry()方法來(lái)清理數(shù)據(jù)。這種設(shè)計(jì)確保了即使沒(méi)有顯式調(diào)用remove()方法,系統(tǒng)也會(huì)在必要時(shí)自動(dòng)清理不再使用的ThreadLocal變量占用的內(nèi)存資源。

需要我們特別注意的是,盡管ThreadLocal提供了remove這種機(jī)制來(lái)防止內(nèi)存泄漏,但它并不會(huì)自動(dòng)執(zhí)行相關(guān)的清理操作。所以為了確保資源有效釋放并避免潛在的內(nèi)存泄露問(wèn)題,我們應(yīng)當(dāng)在完成對(duì)ThreadLocal對(duì)象中數(shù)據(jù)的使用后,及時(shí)調(diào)用其remove()方法。我們最好(也是必須)是在try-finally代碼塊結(jié)構(gòu)中,在finally塊中明確地執(zhí)行remove()方法,這樣即使在處理過(guò)程中拋出異常,也能確保ThreadLocal關(guān)聯(lián)的數(shù)據(jù)被清除,從而有利于GC回收不再使用的內(nèi)存空間,避免內(nèi)存泄漏。

總結(jié)

本文探討了ThreadLocal的工作原理以及其內(nèi)存泄漏問(wèn)題及解決策略。ThreadLocal通過(guò)為每個(gè)線程提供獨(dú)立的變量副本,實(shí)現(xiàn)多線程環(huán)境下的數(shù)據(jù)隔離。其內(nèi)部通過(guò)ThreadLocalMap與當(dāng)前線程綁定,利用弱引用管理鍵值對(duì)。但是,如果未及時(shí)清理不再使用的ThreadLocal變量,可能導(dǎo)致內(nèi)存泄漏,尤其是在線程池場(chǎng)景下。解決辦法包括在完成任務(wù)后調(diào)用remove方法移除無(wú)用數(shù)據(jù)。正確理解和使用ThreadLocal能夠有效提升并發(fā)編程效率,但務(wù)必關(guān)注潛在的內(nèi)存泄漏風(fēng)險(xiǎn)。

責(zé)任編輯:武曉燕 來(lái)源: 碼農(nóng)Academy
相關(guān)推薦

2022-10-18 08:38:16

內(nèi)存泄漏線程

2018-10-25 15:24:10

ThreadLocal內(nèi)存泄漏Java

2023-11-03 08:10:49

ThreadLoca內(nèi)存泄露

2021-08-10 09:58:59

ThreadLocal內(nèi)存泄漏

2022-05-09 14:09:23

多線程線程安全

2020-09-10 07:40:28

ThreadLocal內(nèi)存

2021-02-18 16:53:44

內(nèi)存ThreadLocal線程

2025-04-01 05:22:00

JavaThread變量

2024-02-21 08:00:55

WindowsDWM進(jìn)程

2024-09-29 08:57:25

2023-11-28 12:25:02

多線程安全

2021-04-25 09:58:48

mmapJava面試

2024-10-31 09:24:42

2021-03-17 15:54:32

IO零拷貝方式

2024-01-30 10:12:00

Java內(nèi)存泄漏

2017-01-05 19:34:06

漏洞nodejs代碼

2024-10-24 16:51:08

2021-12-28 14:53:47

Java編程語(yǔ)言

2010-09-26 15:38:33

JVM內(nèi)存泄漏

2022-08-26 07:33:49

內(nèi)存JVMEntry
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)