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

為什么大家都說 ThreadLocal 存在內(nèi)存泄漏的風(fēng)險?

開發(fā) 前端
使用ThreadLocal?時,如果當(dāng)前線程中的變量已經(jīng)使用完畢并且永久不在使用,推薦手動調(diào)用移除remove()?方法,可以采用try ... finally?結(jié)構(gòu),并在finally中清除變量,防止存在潛在的內(nèi)存溢出風(fēng)險。

01、背景介紹

在 Java web 項目中,想必很多的同學(xué)對ThreadLocal這個類并不陌生,它最常用的應(yīng)用場景就是用來做對象的跨層傳遞,避免多次傳遞,打破層次之間的約束。

比如下面這個HttpServletRequest參數(shù)傳遞的簡單例子!

public class RequestLocal {

    /**
     * 線程本地變量
     */
    private static ThreadLocal<HttpServletRequest> local = new ThreadLocal<>();

    /**
     * 存儲請求對象
     * @param request
     */
    public static void set(HttpServletRequest request){
        local.set(request);
    }

    /**
     * 獲取請求對象
     * @return
     */
    public static HttpServletRequest get(){
        return local.get();
    }

    /**
     * 移除請求對象
     * @return
     */
    public static void remove(){
        local.remove();
    }
}
public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 存儲請求對象變量
        RequestLocal.set(req);
        try {

            // 業(yè)務(wù)邏輯...
        } finally {
            // 請求完畢之后,移除請求對象變量
            RequestLocal.remove();
        }
    }
}
// 在需要的地方,通過 RequestLocal 類獲取HttpServletRequest對象
HttpServletRequest request = RequestLocal.get();

看完以上示例,相信大家對ThreadLocal的使用已經(jīng)有了大致的認(rèn)識。

當(dāng)然ThreadLocal的作用還不僅限如此,作為 Java 多線程模塊的一部分,ThreadLocal也經(jīng)常被一些面試官作為知識點用來提問,因此只有理解透徹了,回答才能更加游刃有余。

下面我們從ThreadLocal類的源碼解析到使用方式做一次總結(jié),如果有不正之處,請多多諒解,并歡迎批評指出。

02、源碼剖析

ThreadLocal類,也經(jīng)常被叫做線程本地變量,也有的叫做本地線程變量,意思其實差不多,通俗的解釋:ThreadLocal作用是為變量在每個線程中創(chuàng)建一個副本,這樣每個線程就可以訪問自己內(nèi)部的副本變量;同時,該變量對其他線程而言是封閉且隔離的。

字面的意思很容易理解,但是實際上ThreadLocal類的實現(xiàn)原理還有點復(fù)雜。

打開ThreadLocal類,它總共有 4 個public方法,內(nèi)容如下!

方法

描述

public void set(T value)

設(shè)置當(dāng)前線程變量

public T get()

獲取當(dāng)前線程變量

public void remove()

移除當(dāng)前線程設(shè)置的變量

public static ThreadLocal withInitial(Supplier supplier)

自定義初始化當(dāng)前線程的默認(rèn)值

其中使用最多的就是set()、get()和remove()方法,至于withInitial()方法,一般在ThreadLocal對象初始化的時候,給定一個默認(rèn)值,例如下面這個例子!

// 給所有線程初始化一個變量 1
private static ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 1);

下面我們重點來剖析以上三個方法的源碼,最后總結(jié)如何正確的使用。

以下源碼解析均基于jdk1.8。

2.1、set 方法

打開ThreadLocal類,set()方法的源碼如下!

public void set(T value) {
    // 首先獲取當(dāng)前線程對象
    Thread t = Thread.currentThread();
    // 獲取當(dāng)前線程中的變量 ThreadLocal.ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果不為空,就設(shè)置值
    if (map != null)
        map.set(this, value);
    else
        // 如果為空,初始化一個ThreadLocalMap變量,其中key為當(dāng)前的threadlocal變量
        createMap(t, value);
}

我們繼續(xù)看看createMap()方法的源碼,內(nèi)容如下!

void createMap(Thread t, T firstValue) {
    // 初始化一個 ThreadLocalMap 對象,并賦予給 Thread 對象
    // 可以發(fā)現(xiàn),其實 ThreadLocalMap 是 Thread 類的一個屬性變量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // INITIAL_CAPACITY 變量的初始值為 16
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

從上面的源碼上你會發(fā)現(xiàn),通過ThreadLocal類設(shè)置的變量,最終保存在每個線程自己的ThreadLocal.ThreadLocalMap對象中,其中key是當(dāng)前線程的ThreadLocal變量,value就是我們設(shè)置的變量。

基于這點,可以得出一個結(jié)論:每個線程設(shè)置的變量只有自己可見,其它線程無法訪問,因為這個變量是線程自己獨有的屬性。

從上面的源碼也可以看出,真正負(fù)責(zé)存儲value變量的是Entry靜態(tài)類,并且這個類繼承了一個WeakReference類。稍有不同的是,Entry靜態(tài)類中的key是一個弱引用類型對象,而value是一個強(qiáng)引用類型對象。這樣設(shè)計的好處在于,弱引用的對象更容易被 GC 回收,當(dāng)ThreadLocal對象不再被其他對象使用時,可以被垃圾回收器自動回收,避免可能的內(nèi)存泄漏。關(guān)于這一點,我們在下文再詳細(xì)的介紹。

最后我們再來看看map.set(this, value)這個方法的源碼邏輯,內(nèi)容如下!

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 根據(jù)hash和位運算,計算出數(shù)組中的存儲位置
    int i = key.threadLocalHashCode & (len-1);
    // 循環(huán)遍歷檢查計算出來的位置上是否被占用
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 進(jìn)入循環(huán)體內(nèi),說明當(dāng)前位置已經(jīng)被占用了
        ThreadLocal<?> k = e.get();
        // 如果key相同,直接進(jìn)行覆蓋
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果key為空,說明key被回收了,重新覆蓋
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 當(dāng)沒有被占用,循環(huán)結(jié)束之后,取最后計算的空位,進(jìn)行存儲
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
private static int nextIndex(int i, int len) {
    // 下標(biāo)依次自增
    return ((i + 1 < len) ? i + 1 : 0);
}

從上面的源碼分析可以看出,ThreadLocalMap和HashMap,雖然都是鍵值對的方式存儲數(shù)據(jù),當(dāng)在數(shù)組中存儲數(shù)據(jù)的下表沖突時,存儲數(shù)據(jù)的方式有很大的不同。jdk1.8種的HashMap采用的是鏈表法和紅黑樹來解決下表沖突,當(dāng)

ThreadLocalMap采用的是開放尋址法來解決hash沖突,簡單的說就是當(dāng)hash出來的存儲位置相同但key不一樣時,會繼續(xù)尋找下一個存儲位置,直到找到空位來存儲數(shù)據(jù)。

圖片圖片

而jdk1.7中的HashMap采用的是鏈表法來解決hash沖突,當(dāng)hash出來的存儲位置相同但key不一樣時,會將變量通過鏈表的方式掛在數(shù)組節(jié)點上。

為了實現(xiàn)更高的讀寫效率,jdk1.8中的HashMap就更為復(fù)雜了,當(dāng)沖突的鏈表長度超過 8 時,鏈表會轉(zhuǎn)變成紅黑樹,在此不做過多的講解,有興趣的同學(xué)可以翻看關(guān)于HashMap的源碼分析文章。

一路分析下來,是不是感覺set()方法還是挺復(fù)雜的,總結(jié)下來set()大致的邏輯有以下幾個步驟:

  • 1.首先獲取當(dāng)前線程對象,檢查當(dāng)前線程中的ThreadLocalMap是否存在
  • 2.如果不存在,就給線程創(chuàng)建一個ThreadLocal.ThreadLocalMap對象
  • 3.如果存在,就設(shè)置值,存儲過程中如果存在 hash 沖突時,采用開放尋址法,重新找一個空位進(jìn)行存儲

2.2、get 方法

了解完set()方法之后,get()方法就更容易了,get()方法的源碼如下!

public T get() {
    // 獲取當(dāng)前線程對象
    Thread t = Thread.currentThread();
    // 從當(dāng)前線程對象中獲取 ThreadLocalMap 對象
    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;
        }
    }
    // 如果沒有值,重新初始化默認(rèn)值
    return setInitialValue();
}

這里我們要重點看下 map.getEntry(this)這個方法,源碼如下!

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果找到key,直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 如果找不到,就嘗試清理,如果你總是訪問存在的key,那么這個清理永遠(yuǎn)不會進(jìn)來
        return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        // e指的是entry ,也就是一個弱引用
        ThreadLocal<?> k = e.get();
        // 如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            // 如果key為null,說明已經(jīng)被回收了,同時將value設(shè)置為null,以便進(jìn)行回收
            expungeStaleEntry(i);
        else
            // 如果key不是要找的那個,那說明有hash沖突,繼續(xù)找下一個entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

從上面的源碼可以看出,get()方法邏輯,總共有以下幾個步驟:

  • 1.首先獲取當(dāng)前線程對象,從當(dāng)前線程對象中獲取 ThreadLocalMap 對象
  • 2.然后判斷ThreadLocalMap是否存在,如果存在,就嘗試去獲取最終的value
  • 3.如果不存在,就重新初始化默認(rèn)值,以便清理舊的value值

其中expungeStaleEntry()方法是真正用于清理value值的,setInitialValue()方法也具備清理舊的value變量作用。

從上面的代碼可以看出,ThreadLocal為了清楚value變量,花了不少的心思,其實本質(zhì)都是為了防止ThreadLocal出現(xiàn)可能的內(nèi)存泄漏。

2.3、remove 方法

我們再來看看remove()方法,源碼如下!

public void remove() {
    // 獲取當(dāng)前線程里面的 ThreadLocalMap 對象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 如果不為空,就移除
        m.remove(this);
}
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 循環(huán)遍歷目標(biāo)key,然后將key和value都設(shè)置為null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            // 清理value值
            expungeStaleEntry(i);
            return;
        }
    }
}

remove()方法邏輯比較簡單,首先獲取當(dāng)前線程的ThreadLocalMap對象,然后循環(huán)遍歷key,將目標(biāo)key以及對應(yīng)的value都設(shè)置為null。

從以上的源碼剖析中,可以得出一個結(jié)論:不管是set()、get()還是remove(),其實都會主動清理無效的value數(shù)據(jù),因此實際開發(fā)過程中,沒有必要過于擔(dān)心內(nèi)存泄漏的問題。

03、為什么要用 WeakReference?

另外細(xì)心的同學(xué)可能會發(fā)現(xiàn),ThreadLocal中真正負(fù)責(zé)存儲key和value變量的是Entry靜態(tài)類,并且它繼承了一個WeakReference類。

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)于WeakReference類,我們在上文只是簡單的說了一下,可能有的同學(xué)不太清楚,這個再次簡要的介紹一下。

了解過WeakHashMap類的同學(xué),可能對WeakReference有印象,它表示當(dāng)前對象為弱引用類型。

在 Java 中,對象有四種引用類型,分別是:強(qiáng)引用、軟引用、弱引用和虛引用,級別從高依次到低。

不同引用類型的對象,GC 回收的方式也不一樣,對于強(qiáng)引用類型,不會被垃圾收集器回收,即使當(dāng)內(nèi)存不足時,另可拋異常也不會主動回收,防止程序出現(xiàn)異常,通常我們自定義的類,初始化的對象都是強(qiáng)引用類型;對于軟引用類型的對象,當(dāng)不存在外部強(qiáng)引用的時候,GC 會在內(nèi)存不足的時候,進(jìn)行回收;對于弱引用類型的對象,當(dāng)不存在外部強(qiáng)引用的時候,GC 掃描到時會進(jìn)行回收;對于虛引用,GC 會在任何時候都可能進(jìn)行回收。

下面我們看一個簡單的示例,更容易直觀的了解它。

public static void main(String[] args) {
        Map weakHashMap = new WeakHashMap();
        //向weakHashMap中添加4個元素
        for (int i = 0; i < 3; i++) {
            weakHashMap.put("key-"+i, "value-"+ i);
        }
        //輸出添加的元素
        System.out.println("數(shù)組長度:"+weakHashMap.size() + ",輸出結(jié)果:" + weakHashMap);
        //主動觸發(fā)一次GC
        System.gc();
        //再輸出添加的元素
        System.out.println("數(shù)組長度:"+weakHashMap.size() + ",輸出結(jié)果:" + weakHashMap);
    }

輸出結(jié)果:

數(shù)組長度:3,輸出結(jié)果:{key-2=value-2, key-1=value-1, key-0=value-0}
數(shù)組長度:3,輸出結(jié)果:{}

以上存儲的弱引用對象,與外部對象沒有強(qiáng)關(guān)聯(lián),當(dāng)主動調(diào)用 GC 回收器的時候,再次查詢WeakHashMap里面的數(shù)據(jù)的時候,弱引用對象收回,所以內(nèi)容為空。其中WeakHashMap類底層使用的數(shù)據(jù)存儲對象,也是繼承了WeakReference。

采用WeakReference這種弱引用的方式,當(dāng)不存在外部強(qiáng)引用的時候,就會被垃圾收集器自動回收掉,減小內(nèi)存空間壓力。

需要注意的是,Entry靜態(tài)類中僅僅只是key被設(shè)計成弱引用類型,value依然是強(qiáng)引用類型。

回歸正題,為什么ThreadLocalMap類中的Entry靜態(tài)類中的key需要被設(shè)計成弱引用類型?

我們先看一張Entry對象的依賴圖!

圖片圖片

如上圖所示,Entry持有ThreadLocal對象的引用,如果沒有設(shè)置引用類型,這個引用鏈就全是強(qiáng)引用,當(dāng)線程沒有結(jié)束時,它持有的強(qiáng)引用,包括遞歸下去的所有強(qiáng)引用都不會被垃圾回收器回收;只有當(dāng)線程生命周期結(jié)束時,才會被回收。

哪怕顯式的設(shè)置threadLocal = null,它也無法被垃圾收集器回收,因為Entry和key存在強(qiáng)關(guān)聯(lián)!

如果Entry中的key設(shè)置成弱引用,當(dāng)threadLocal = null時,key就可以被垃圾收集器回收,進(jìn)一步減少內(nèi)存使用空間。

但是也僅僅只是回收key,不能回收value,如果這個線程運行時間非常長,又沒有調(diào)用set()、get()或者remove()方法,隨著線程數(shù)的增多可能會有內(nèi)存溢出的風(fēng)險。

因此在實際的使用中,想要徹底回收value,使用完之后可以顯式調(diào)用一下remove()方法。

04、應(yīng)用介紹

通過以上的源碼分析,相信大家對ThreadLocal類已經(jīng)有了一些認(rèn)識,它主要的作用是在線程內(nèi)實現(xiàn)變量的傳遞,每個線程只能看到自己設(shè)定的變量。

我們可以看一個簡單的示例!

public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    threadLocal.set("main");

    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 設(shè)置變量
                threadLocal.set(String.valueOf(j));
                // 獲取設(shè)置的變量
                System.out.println("thread name:" + Thread.currentThread().getName() + ", 內(nèi)容:" + threadLocal.get());
            }
        }).start();
    }
    
    System.out.println("thread name:" + Thread.currentThread().getName() + ", 內(nèi)容:" + threadLocal.get());
}

輸出結(jié)果:

thread name:Thread-0, 內(nèi)容:0
thread name:Thread-1, 內(nèi)容:1
thread name:Thread-2, 內(nèi)容:2
thread name:Thread-3, 內(nèi)容:3
thread name:main, 內(nèi)容:main
thread name:Thread-4, 內(nèi)容:4

從運行結(jié)果上可以很清晰的看出,每個線程只能看到自己設(shè)置的變量,其它線程不可見。

ThreadLocal可以實現(xiàn)線程之間的數(shù)據(jù)隔離,在實際的業(yè)務(wù)開發(fā)中,使用非常廣泛,例如文章開頭介紹的HttpServletRequest參數(shù)的上下文傳遞。

05、小結(jié)

最后我們來總結(jié)一下,ThreadLocal類經(jīng)常被叫做線程本地變量,它確保每個線程的ThreadLocal變量都是各自獨立的,其它線程無法訪問,實現(xiàn)線程之間數(shù)據(jù)隔離的效果。

ThreadLocal適合在一個線程的處理流程中實現(xiàn)參數(shù)上下文的傳遞,避免同一個參數(shù)在所有的方法中傳遞。

使用ThreadLocal時,如果當(dāng)前線程中的變量已經(jīng)使用完畢并且永久不在使用,推薦手動調(diào)用移除remove()方法,可以采用try ... finally結(jié)構(gòu),并在finally中清除變量,防止存在潛在的內(nèi)存溢出風(fēng)險。

06、參考

1、https://www.cnblogs.com/xrq730/p/4854813.html

2、https://www.cnblogs.com/xrq730/p/4854820.html

3、https://zhuanlan.zhihu.com/p/102744180

責(zé)任編輯:武曉燕 來源: 潘志的研發(fā)筆記
相關(guān)推薦

2021-08-10 09:58:59

ThreadLocal內(nèi)存泄漏

2025-04-01 05:22:00

JavaThread變量

2020-07-06 08:15:59

SQLSELECT優(yōu)化

2018-10-25 15:24:10

ThreadLocal內(nèi)存泄漏Java

2022-05-09 14:09:23

多線程線程安全

2019-05-23 10:59:24

Java內(nèi)存 C++

2024-03-22 13:31:00

線程策略線程池

2022-10-18 08:38:16

內(nèi)存泄漏線程

2020-09-10 07:40:28

ThreadLocal內(nèi)存

2021-02-18 16:53:44

內(nèi)存ThreadLocal線程

2011-05-24 16:39:09

Cfree()

2020-11-04 13:01:38

FastThreadLocalJDK

2022-07-26 07:14:20

線程隔離Thread

2024-03-22 12:29:03

HashMap線程

2021-08-10 16:50:37

內(nèi)核內(nèi)存管理

2023-05-29 07:17:48

內(nèi)存溢出場景

2021-03-10 09:40:50

Linux命令文件

2017-12-15 14:10:20

深度學(xué)習(xí)本質(zhì)邊緣識別

2019-11-06 19:21:07

Pythonargparse解釋器

2020-09-11 07:38:50

內(nèi)存泄漏檢測
點贊
收藏

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