來源:blog.csdn.net/zzg1229059735/article/details/82715741
本次給大家介紹重要的工具ThreadLocal,講解內容如下,同時介紹什么場景下發生記憶體泄漏,如何復現記憶體泄漏,如何正確使用它來避免記憶體泄漏,
- ThreadLocal是什么?有哪些用途?
- ThreadLocal如何使用
- 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來給開發者分配任務,下面再舉個栗子,進一步說明他們之間的關系:

- 每個人都一張銀行卡
- 每個人每張卡都有一定的余額,
- 每個人獲取銀行卡余額都必須通過該銀行的管理系統,
- 每個人都只能獲取自己卡持有的余額資訊,他人的不可訪問,

映射到我們要說的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理解不透徹的開發者,不敢貿然使用,越不用,越陌生,這樣就讓我們錯失了更好的實作方案,所以敢于引入新技術,敢于踩坑,才能不斷進步,
我們來看下為什么說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方法,
-
- 最簡單有效的方法是使用后將其移除,
以上,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.Spring Boot 2.6 正式發布,一大波新特性,,
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/404259.html
標籤:Java
