全方位,多角度理解 ThreadLocal
本次給大家介紹重要的工具 ThreadLocal。講解內容如下,同時介紹什麼場景下發生內存泄漏,如何復現內存泄漏,如何正確使用它來避免內存泄漏。
-
ThreadLocal 是什麼?有哪些用途?
-
ThreadLocal 如何使用
-
ThreadLocal 原理
-
ThreadLocal 使用有哪些坑及注意事項
- 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 來給開發者分配任務。下面再舉個栗子,進一步說明他們之間的關係:
-
每個人都一張銀行卡
-
每個人每張卡都有一定的餘額。
-
每個人獲取銀行卡餘額都必須通過該銀行的管理系統。
-
每個人都只能獲取自己卡持有的餘額信息,他人的不可訪問。
映射到我們要說的 ThreadLocal
-
card 類似於 Thread
-
card 餘額屬性,卡號屬性等類似於 Treadlocal 內部屬性集合 threadLocals
-
cardManager 類似於 ThreadLocal 管理類
那 ThreadLocal 有哪些應用場景呢?
其實我們無意間已經時時刻刻在使用 ThreadLocal 提供的便利,如果說多數據源的切換你比較陌生,那麼 spring 提供的聲明式事務就再熟悉不過了,我們在研發過程中無時無刻不在使用,而 spring 聲明式事務的重要實現基礎就是 ThreadLocal,只不過大家沒有去深入研究 spring 聲明式事務的實現機制。後面有機會我會給大家介紹 spring 聲明式事務的原理及實現機制。
原來 ThreadLocal 這麼強大,但應用開發者使用較少,同時有些研發人員對於 ThreadLocal 內存泄漏,等潛在問題,不敢試用,恐怕這是對於 ThreadLocal 最大的誤解,後面我們將會仔細分析,只要按照正確使用方式,就沒什麼問題。如果 ThreadLocal 存在問題,豈不是 spring 聲明式事務是我們程序最大的潛在危險嗎?
2.ThreadLocal 如何使用
爲了更直觀的體會 ThreadLocal 的使用我們假設如下場景
-
我們給每個線程生成一個 ID。
-
一旦設置,線程生命週期內不可變化。
-
容器活動期間不可以生成重複的 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 會引起內存泄漏,什麼場景下會導致內存泄漏?
先回顧下什麼叫內存泄漏,對應的什麼叫內存溢出
-
①Memory overflow: 內存溢出,沒有足夠的內存提供申請者使用。
-
②Memory leak: 內存泄漏,程序申請內存後,無法釋放已申請的內存空間,內存泄漏的堆積終將導致內存溢出。
顯然是 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 引用被設置爲 null,且後面沒有 set,get,remove 操作。
-
②線程一直運行,不停止。(線程池)
-
③觸發了垃圾回收。(Minor GC 或 Full GC)
我們看到 ThreadLocal 出現內存泄漏條件還是很苛刻的,所以我們只要破壞其中一個條件就可以避免內存泄漏,單但爲了更好的避免這種情況的發生我們使用 ThreadLocal 時遵守以下兩個小原則:
-
①ThreadLocal 申明爲 private static final。
-
Private 與 final 儘可能不讓他人修改變更引用,
-
Static 表示爲類屬性,只有在程序結束纔會被回收。
-
②ThreadLocal 使用後務必調用 remove 方法。
-
最簡單有效的方法是使用後將其移除。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BnMobn2DRaZabBfApipOdg