成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

為什么大家都說 ThreadLocal 存在內存泄漏的風險?

開發 前端
使用ThreadLocal?時,如果當前線程中的變量已經使用完畢并且永久不在使用,推薦手動調用移除remove()?方法,可以采用try ... finally?結構,并在finally中清除變量,防止存在潛在的內存溢出風險。

01、背景介紹

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

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

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 {

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

看完以上示例,相信大家對ThreadLocal的使用已經有了大致的認識。

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

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

02、源碼剖析

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

字面的意思很容易理解,但是實際上ThreadLocal類的實現原理還有點復雜。

打開ThreadLocal類,它總共有 4 個public方法,內容如下!

方法

描述

public void set(T value)

設置當前線程變量

public T get()

獲取當前線程變量

public void remove()

移除當前線程設置的變量

public static ThreadLocal withInitial(Supplier supplier)

自定義初始化當前線程的默認值

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

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

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

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

2.1、set 方法

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

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

我們繼續看看createMap()方法的源碼,內容如下!

void createMap(Thread t, T firstValue) {
    // 初始化一個 ThreadLocalMap 對象,并賦予給 Thread 對象
    // 可以發現,其實 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;
    }
}

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

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

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

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

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

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

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

圖片圖片

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

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

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

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

2.2、get 方法

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

public T get() {
    // 獲取當前線程對象
    Thread t = Thread.currentThread();
    // 從當前線程對象中獲取 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;
        }
    }
    // 如果沒有值,重新初始化默認值
    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,那么這個清理永遠不會進來
        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,說明已經被回收了,同時將value設置為null,以便進行回收
            expungeStaleEntry(i);
        else
            // 如果key不是要找的那個,那說明有hash沖突,繼續找下一個entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

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

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

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

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

2.3、remove 方法

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

public void remove() {
    // 獲取當前線程里面的 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);
    // 循環遍歷目標key,然后將key和value都設置為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()方法邏輯比較簡單,首先獲取當前線程的ThreadLocalMap對象,然后循環遍歷key,將目標key以及對應的value都設置為null。

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

03、為什么要用 WeakReference?

另外細心的同學可能會發現,ThreadLocal中真正負責存儲key和value變量的是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;
    }
}

關于WeakReference類,我們在上文只是簡單的說了一下,可能有的同學不太清楚,這個再次簡要的介紹一下。

了解過WeakHashMap類的同學,可能對WeakReference有印象,它表示當前對象為弱引用類型。

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

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

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

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("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
        //主動觸發一次GC
        System.gc();
        //再輸出添加的元素
        System.out.println("數組長度:"+weakHashMap.size() + ",輸出結果:" + weakHashMap);
    }

輸出結果:

數組長度:3,輸出結果:{key-2=value-2, key-1=value-1, key-0=value-0}
數組長度:3,輸出結果:{}

以上存儲的弱引用對象,與外部對象沒有強關聯,當主動調用 GC 回收器的時候,再次查詢WeakHashMap里面的數據的時候,弱引用對象收回,所以內容為空。其中WeakHashMap類底層使用的數據存儲對象,也是繼承了WeakReference。

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

需要注意的是,Entry靜態類中僅僅只是key被設計成弱引用類型,value依然是強引用類型。

回歸正題,為什么ThreadLocalMap類中的Entry靜態類中的key需要被設計成弱引用類型?

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

圖片圖片

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

哪怕顯式的設置threadLocal = null,它也無法被垃圾收集器回收,因為Entry和key存在強關聯!

如果Entry中的key設置成弱引用,當threadLocal = null時,key就可以被垃圾收集器回收,進一步減少內存使用空間。

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

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

04、應用介紹

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

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

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() {
                // 設置變量
                threadLocal.set(String.valueOf(j));
                // 獲取設置的變量
                System.out.println("thread name:" + Thread.currentThread().getName() + ", 內容:" + threadLocal.get());
            }
        }).start();
    }
    
    System.out.println("thread name:" + Thread.currentThread().getName() + ", 內容:" + threadLocal.get());
}

輸出結果:

thread name:Thread-0, 內容:0
thread name:Thread-1, 內容:1
thread name:Thread-2, 內容:2
thread name:Thread-3, 內容:3
thread name:main, 內容:main
thread name:Thread-4, 內容:4

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

ThreadLocal可以實現線程之間的數據隔離,在實際的業務開發中,使用非常廣泛,例如文章開頭介紹的HttpServletRequest參數的上下文傳遞。

05、小結

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

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

使用ThreadLocal時,如果當前線程中的變量已經使用完畢并且永久不在使用,推薦手動調用移除remove()方法,可以采用try ... finally結構,并在finally中清除變量,防止存在潛在的內存溢出風險。

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

責任編輯:武曉燕 來源: 潘志的研發筆記
相關推薦

2021-08-10 09:58:59

ThreadLocal內存泄漏

2025-04-01 05:22:00

JavaThread變量

2020-07-06 08:15:59

SQLSELECT優化

2018-10-25 15:24:10

ThreadLocal內存泄漏Java

2022-05-09 14:09:23

多線程線程安全

2019-05-23 10:59:24

Java內存 C++

2024-03-22 13:31:00

線程策略線程池

2022-10-18 08:38:16

內存泄漏線程

2020-09-10 07:40:28

ThreadLocal內存

2011-05-24 16:39:09

Cfree()

2021-02-18 16:53:44

內存ThreadLocal線程

2020-11-04 13:01:38

FastThreadLocalJDK

2022-07-26 07:14:20

線程隔離Thread

2024-03-22 12:29:03

HashMap線程

2023-05-29 07:17:48

內存溢出場景

2021-08-10 16:50:37

內核內存管理

2021-03-10 09:40:50

Linux命令文件

2019-11-06 19:21:07

Pythonargparse解釋器

2017-12-15 14:10:20

深度學習本質邊緣識別

2020-09-11 07:38:50

內存泄漏檢測
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久99蜜桃综合影院免费观看 | 午夜精品一区二区三区在线播放 | 噜噜噜噜狠狠狠7777视频 | 91在线| 成人av播放| h视频免费观看 | 欧美日韩综合一区 | 欧美日韩国产三级 | 亚洲精品乱码8久久久久久日本 | 亚洲a在线观看 | 天堂亚洲| 四虎影视在线 | 日韩 欧美 二区 | 日韩一级免费观看 | 怡红院成人在线视频 | 五月婷婷视频 | 欧美成人不卡 | 午夜精品久久久久久不卡欧美一级 | 噜噜噜色网 | 国产精品一码二码三码在线 | 91看国产 | 操久久 | 在线黄色影院 | 欧美久久一级特黄毛片 | www.99re | 国产视频一区二区 | 亚洲成av人影片在线观看 | 国产欧美在线 | 国产精品久久久久久52avav | 欧美激情精品久久久久久免费 | 日韩三级一区 | 国产中文字幕在线 | 精品99久久 | 精品国产乱码久久久久久牛牛 | 玖玖精品| 日韩日b视频 | 久久国产欧美日韩精品 | 国产精品久久久久久久久久免费 | 国产视频福利一区 | 国产激情一区二区三区 | 日韩有码一区 |