全方位,多角度理解 ThreadLocal

本次給大家介紹重要的工具 ThreadLocal。講解內容如下,同時介紹什麼場景下發生內存泄漏,如何復現內存泄漏,如何正確使用它來避免內存泄漏。

  1. ThreadLocal 是什麼?有哪些用途?

首先介紹 Thread 類中屬性 threadLocals:

/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

我們發現 Thread 並沒有提供成員變量 threadLocals 的設置與訪問的方法,那麼每個線程的實例 threadLocals 參數我們如何操作呢?這時我們的主角:ThreadLocal 就登場了。

所以有那麼一句總結:ThreadLocal 是線程 Thread 中屬性 threadLocals 的管理者。

也就是說我們對於 ThreadLocal 的 get, set,remove 的操作結果都是針對當前線程 Thread 實例的 threadLocals 存,取,刪除操作。類似於一個開發者的任務,產品經理左右不了,產品經理只能通過技術 leader 來給開發者分配任務。下面再舉個栗子,進一步說明他們之間的關係:

  1. 每個人都一張銀行卡

  2. 每個人每張卡都有一定的餘額。

  3. 每個人獲取銀行卡餘額都必須通過該銀行的管理系統。

  4. 每個人都只能獲取自己卡持有的餘額信息,他人的不可訪問。

映射到我們要說的 ThreadLocal

  1. card 類似於 Thread

  2. card 餘額屬性,卡號屬性等類似於 Treadlocal 內部屬性集合 threadLocals

  3. cardManager 類似於 ThreadLocal 管理類

那 ThreadLocal 有哪些應用場景呢?

其實我們無意間已經時時刻刻在使用 ThreadLocal 提供的便利,如果說多數據源的切換你比較陌生,那麼 spring 提供的聲明式事務就再熟悉不過了,我們在研發過程中無時無刻不在使用,而 spring 聲明式事務的重要實現基礎就是 ThreadLocal,只不過大家沒有去深入研究 spring 聲明式事務的實現機制。後面有機會我會給大家介紹 spring 聲明式事務的原理及實現機制。

原來 ThreadLocal 這麼強大,但應用開發者使用較少,同時有些研發人員對於 ThreadLocal 內存泄漏,等潛在問題,不敢試用,恐怕這是對於 ThreadLocal 最大的誤解,後面我們將會仔細分析,只要按照正確使用方式,就沒什麼問題。如果 ThreadLocal 存在問題,豈不是 spring 聲明式事務是我們程序最大的潛在危險嗎?

2.ThreadLocal 如何使用

爲了更直觀的體會 ThreadLocal 的使用我們假設如下場景

  1. 我們給每個線程生成一個 ID。

  2. 一旦設置,線程生命週期內不可變化。

  3. 容器活動期間不可以生成重複的 ID

我們創建一個 ThreadLocal 管理類:

測試程序如下:我們同一個線程不斷 get,測試 id 是否變化,同時測試完成後我們就將其釋放掉。

在主程序中我們開啓多個線程測試不通線程之間是否會影響

不出意外我們的結果爲:

結果:確實是不同線程間 id 不同,相同線程 id 相同。

3.ThreadLocal 原理

①ThreadLocal 類結構及方法解析:

上圖可知:ThreadLocal 三個方法 get, set , remove 以及內部類ThreadLocalMap

②ThreadLocal 及 Thread 之間的關係:

從這張圖我們可以直觀的看到 Thread 中屬性 threadLocals,作爲一個特殊的 Map,它的 key 值就是我們 ThreadLocal 實例,而 value 值這是我們設置的值。

③ThreadLocal 的操作過程:

我們以 get 方法爲例:

其中 getMap(t) 返回的就上當前線程的 threadlocals,如下圖,然後根據當前 ThreadLocal 實例對象作爲 key 獲取 ThreadLocalMap 中的 value,如果首次進來這調用 setInitialValue()

set 的過程也類似:

注意:ThreadLocal 中可以直接 t.threadLocals 是因爲 Thread 與 ThreadLocal 在同一個包下,同樣 Thread 可以直接訪問 ThreadLocal.ThreadLocalMap threadLocals = null; 來進行聲明屬性。

4.ThreadLocal 使用有哪些坑及注意事項

我經常在網上看到駭人聽聞的標題,ThreadLocal 導致內存泄漏,這通常讓一些剛開始對 ThreadLocal 理解不透徹的開發者,不敢貿然使用。越不用,越陌生。這樣就讓我們錯失了更好的實現方案,所以敢於引入新技術,敢於踩坑,才能不斷進步。另外,關注 Java 知音公衆號,回覆 “後端面試”,送你一份面試題寶典!

我們來看下爲什麼說 ThreadLocal 會引起內存泄漏,什麼場景下會導致內存泄漏?

先回顧下什麼叫內存泄漏,對應的什麼叫內存溢出

顯然是 TreadLocal 在不規範使用的情況下導致了內存沒有釋放。

紅框裏我們看到了一個特殊的類 WeakReference,同樣這個類,應用開發者也同樣很少使用,這裏簡單介紹下吧

既然 WeakReference 在下一次 gc 即將被回收,那麼我們的程序爲什麼沒有出問題呢?

①所以我們測試下弱引用的回收機制:

這一種存在強引用不會被回收。

這裏沒有強引用將會被回收。

上面演示了弱引用的回收情況,下面我們看下 ThreadLocal 的弱引用回收情況。

②ThreadLocal 的弱引用回收情況

如上圖所示,我們在作爲 key 的 ThreadLocal 對象沒有外部強引用,下一次 gc 必將產生 key 值爲 null 的數據,若線程沒有及時結束必然出現,一條強引用鏈Threadref–>Thread–>ThreadLocalMap–>Entry,所以這將導致內存泄漏。推薦:面試題倉庫

下面我們模擬復現 ThreadLocal 導致內存泄漏:

1. 爲了效果更佳明顯我們將我們的 treadlocals 的存儲值 value 設置爲 1 萬字符串的列表:

class ThreadLocalMemory {
    // Thread local variable containing each thread's ID
    public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
        @Override
        protected List<Object> initialValue() {
            List<Object> list = new ArrayList<Object>();
            for (int i = 0; i < 10000; i++) {
                list.add(String.valueOf(i));
            }
            return list;
        }
    };
    // Returns the current thread's unique ID, assigning it if necessary
    public List<Object> get() {
        return threadId.get();
    }
    // remove currentid
    public void remove() {
        threadId.remove();
    }
}

測試代碼如下:

public static void main(String[] args)
            throws InterruptedException {

        //  爲了復現key被回收的場景,我們使用臨時變量
        ThreadLocalMemory memeory = new ThreadLocalMemory();

        // 調用
        incrementSameThreadId(memeory);

        System.out.println("GC前:key:" + memeory.threadId);
        System.out.println("GC前:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 設置爲null,調用gc並不一定觸發垃圾回收,但是可以通過java提供的一些工具進行手工觸發gc回收。
        memeory.threadId = null;
        System.gc();

        System.out.println("GC後:key:" + memeory.threadId);
        System.out.println("GC後:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 模擬線程一直運行
        while (true) {
        }
    }

此時我們如何知道內存中存在 memory leak 呢?

我們可以藉助 jdk 提供的一些命令 dump 當前堆內存,命令如下:

jmap -dump:live,format=b,file=heap.bin <pid>

然後我們藉助 MAT 可視化分析工具,來查看對內存,分析對象實例的存活狀態:

首先打開我們工具提示我們的內存泄漏分析:

這裏我們可以確定的是 ThreadLocalMap 實例的 Entry.value 是沒有被回收的。

最後我們要確定 Entry.key 是否還在?打開 Dominator Tree,搜索我們的 ThreadLocalMemory,發現並沒有存活的實例。

以上我們復現了 ThreadLocal 不正當使用,引起的內存泄漏。demo 在這裏。

所以我們總結了使用 ThreadLocal 時會發生內存泄漏的前提條件:

我們看到 ThreadLocal 出現內存泄漏條件還是很苛刻的,所以我們只要破壞其中一個條件就可以避免內存泄漏,單但爲了更好的避免這種情況的發生我們使用 ThreadLocal 時遵守以下兩個小原則:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BnMobn2DRaZabBfApipOdg