本次給大家介紹重要的工具ThreadLocal。講解內(nèi)容如下,同時介紹什么場景下發(fā)生內(nèi)存泄漏,如何復(fù)現(xiàn)內(nèi)存泄漏,如何正確使用它來避免內(nèi)存泄漏。
ThreadLocal是什么?有哪些用途?
ThreadLocal如何使用
ThreadLocal原理
ThreadLocal使用有哪些坑及注意事項(xiàng)
Part1ThreadLocal是什么?有哪些用途?
首先介紹Thread類中屬性threadLocals:
/* ThreadLocal values pertaining to this thread.
This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們發(fā)現(xiàn)Thread并沒有提供成員變量threadLocals的設(shè)置與訪問的方法,那么每個線程的實(shí)例threadLocals參數(shù)我們?nèi)绾尾僮髂兀窟@時我們的主角:ThreadLocal就登場了。
所以有那么一句總結(jié):
ThreadLocal是線程Thread中屬性threadLocals的管理者。
也就是說我們對于ThreadLocal的get, set,remove的操作結(jié)果都是針對當(dāng)前線程Thread實(shí)例的threadLocals存,取,刪除操作。類似于一個開發(fā)者的任務(wù),產(chǎn)品經(jīng)理左右不了,產(chǎn)品經(jīng)理只能通過技術(shù)leader來給開發(fā)者分配任務(wù)。
下面再舉個栗子,進(jìn)一步說明他們之間的關(guān)系:
1. 每個人都一張銀行卡
每個人每張卡都有一定的余額。
每個人獲取銀行卡余額都必須通過該銀行的管理系統(tǒng)。
每個人都只能獲取自己卡持有的余額信息,他人的不可訪問。
映射到我們要說的ThreadLocal
card類似于Thread
card余額屬性,卡號屬性等類似于Treadlocal內(nèi)部屬性集合threadLocals
cardManager類似于ThreadLocal管理類
那ThreadLocal有哪些應(yīng)用場景呢?
其實(shí)我們無意間已經(jīng)時時刻刻在使用ThreadLocal提供的便利,如果說多數(shù)據(jù)源的切換你比較陌生,那么spring提供的聲明式事務(wù)就再熟悉不過了,我們在研發(fā)過程中無時無刻不在使用,而spring聲明式事務(wù)的重要實(shí)現(xiàn)基礎(chǔ)就是ThreadLocal,只不過大家沒有去深入研究spring聲明式事務(wù)的實(shí)現(xiàn)機(jī)制。后面有機(jī)會我會給大家介紹spring聲明式事務(wù)的原理及實(shí)現(xiàn)機(jī)制。
原來ThreadLocal這么強(qiáng)大,但應(yīng)用開發(fā)者使用較少,同時有些研發(fā)人員對于ThreadLocal內(nèi)存泄漏,等潛在問題,不敢試用,恐怕這是對于ThreadLocal最大的誤解,后面我們將會仔細(xì)分析,只要按照正確使用方式,就沒什么問題。如果ThreadLocal存在問題,豈不是spring聲明式事務(wù)是我們程序最大的潛在危險嗎?
Part2ThreadLocal如何使用
為了更直觀的體會ThreadLocal的使用我們假設(shè)如下場景
我們給每個線程生成一個ID。
一旦設(shè)置,線程生命周期內(nèi)不可變化。
容器活動期間不可以生成重復(fù)的ID
我們創(chuàng)建一個ThreadLocal管理類:

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

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

不出意外我們的結(jié)果為:

結(jié)果:確實(shí)是不同線程間id不同,相同線程id相同。
Part3ThreadLocal原理
①ThreadLocal類結(jié)構(gòu)及方法解析:

上圖可知:ThreadLocal三個方法get, set , remove以及內(nèi)部類ThreadLocalMap
②ThreadLocal及Thread之間的關(guān)系:

從這張圖我們可以直觀的看到Thread中屬性threadLocals,作為一個特殊的Map,它的key值就是我們ThreadLocal實(shí)例,而value值這是我們設(shè)置的值。
③ThreadLocal的操作過程:
我們以get方法為例:

其中g(shù)etMap(t)返回的就上當(dāng)前線程的threadlocals,如下圖,然后根據(jù)當(dāng)前ThreadLocal實(shí)例對象作為key獲取ThreadLocalMap中的value,如果首次進(jìn)來這調(diào)用setInitialValue()


set的過程也類似:

注意:ThreadLocal中可以直接t.threadLocals是因?yàn)門hread與ThreadLocal在同一個包下,同樣Thread可以直接訪問ThreadLocal.ThreadLocalMap threadLocals = null;來進(jìn)行聲明屬性。另外,歡迎關(guān)注公眾號Java筆記蝦,后臺回復(fù)“后端面試”,送你一份面試題寶典!
Part4ThreadLocal使用有哪些坑及注意事項(xiàng)
我經(jīng)常在網(wǎng)上看到駭人聽聞的標(biāo)題,ThreadLocal導(dǎo)致內(nèi)存泄漏,這通常讓一些剛開始對ThreadLocal理解不透徹的開發(fā)者,不敢貿(mào)然使用。越不用,越陌生。這樣就讓我們錯失了更好的實(shí)現(xiàn)方案,所以敢于引入新技術(shù),敢于踩坑,才能不斷進(jìn)步。
我們來看下為什么說ThreadLocal會引起內(nèi)存泄漏,什么場景下會導(dǎo)致內(nèi)存泄漏?
先回顧下什么叫內(nèi)存泄漏,對應(yīng)的什么叫內(nèi)存溢出
Memory overflow:內(nèi)存溢出,沒有足夠的內(nèi)存提供申請者使用。
Memory leak:內(nèi)存泄漏,程序申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,內(nèi)存泄漏的堆積終將導(dǎo)致內(nèi)存溢出。
顯然是TreadLocal在不規(guī)范使用的情況下導(dǎo)致了內(nèi)存沒有釋放。

紅框里我們看到了一個特殊的類WeakReference,同樣這個類,應(yīng)用開發(fā)者也同樣很少使用,這里簡單介紹下吧

既然WeakReference在下一次gc即將被回收,那么我們的程序?yàn)槭裁礇]有出問題呢?
①所以我們測試下弱引用的回收機(jī)制:

這一種存在強(qiáng)引用不會被回收。

這里沒有強(qiáng)引用將會被回收。
上面演示了弱引用的回收情況,下面我們看下ThreadLocal的弱引用回收情況。
②ThreadLocal的弱引用回收情況

如上圖所示,我們在作為key的ThreadLocal對象沒有外部強(qiáng)引用,下一次gc必將產(chǎn)生key值為null的數(shù)據(jù),若線程沒有及時結(jié)束必然出現(xiàn),一條強(qiáng)引用鏈Threadref–》Thread–》ThreadLocalMap–》Entry,所以這將導(dǎo)致內(nèi)存泄漏。
下面我們模擬復(fù)現(xiàn)ThreadLocal導(dǎo)致內(nèi)存泄漏:
1.為了效果更佳明顯我們將我們的treadlocals的存儲值value設(shè)置為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 {
// 為了復(fù)現(xiàn)key被回收的場景,我們使用臨時變量
ThreadLocalMemory memeory = new ThreadLocalMemory();
// 調(diào)用
incrementSameThreadId(memeory);
System.out.println(“GC前:key:” + memeory.threadId);
System.out.println(“GC前:value-size:” + refelectThreadLocals(Thread.currentThread()));
// 設(shè)置為null,調(diào)用gc并不一定觸發(fā)垃圾回收,但是可以通過java提供的一些工具進(jìn)行手工觸發(fā)gc回收。
memeory.threadId = null;
System.gc();
System.out.println(“GC后:key:” + memeory.threadId);
System.out.println(“GC后:value-size:” + refelectThreadLocals(Thread.currentThread()));
// 模擬線程一直運(yùn)行
while (true) {
}
}
此時我們?nèi)绾沃纼?nèi)存中存在memory leak呢?
我們可以借助jdk提供的一些命令dump當(dāng)前堆內(nèi)存,命令如下:
jmap -dump:live,format=b,file=heap.bin 《pid》
然后我們借助MAT可視化分析工具,來查看對內(nèi)存,分析對象實(shí)例的存活狀態(tài):
首先打開我們工具提示我們的內(nèi)存泄漏分析:
這里我們可以確定的是ThreadLocalMap實(shí)例的Entry.value是沒有被回收的。
最后我們要確定Entry.key是否還在?打開Dominator Tree,搜索我們的ThreadLocalMemory,發(fā)現(xiàn)并沒有存活的實(shí)例。
以上我們復(fù)現(xiàn)了ThreadLocal不正當(dāng)使用,引起的內(nèi)存泄漏。
文中源碼:
https://github.com/z-one/basictest/tree/master/src/main/java/com/spring/test/threadlocal
所以我們總結(jié)了使用ThreadLocal時會發(fā)生內(nèi)存泄漏的前提條件:
ThreadLocal引用被設(shè)置為null,且后面沒有set,get,remove操作。
線程一直運(yùn)行,不停止。(線程池)
觸發(fā)了垃圾回收。(Minor GC或Full GC)
我們看到ThreadLocal出現(xiàn)內(nèi)存泄漏條件還是很苛刻的,所以我們只要破壞其中一個條件就可以避免內(nèi)存泄漏,單但為了更好的避免這種情況的發(fā)生我們使用ThreadLocal時遵守以下兩個小原則:
①ThreadLocal申明為private static final。
Private與final 盡可能不讓他人修改變更引用,
Static 表示為類屬性,只有在程序結(jié)束才會被回收。
②ThreadLocal使用后務(wù)必調(diào)用remove方法。
最簡單有效的方法是使用后將其移除。
責(zé)任編輯:haq
-
內(nèi)存
+關(guān)注
關(guān)注
9文章
3210瀏覽量
76361 -
內(nèi)存泄露
+關(guān)注
關(guān)注
0文章
7瀏覽量
2165
原文標(biāo)題:ThreadLocal理解, 運(yùn)用以及內(nèi)存泄漏的處理方案
文章出處:【微信號:harmonyos_developer,微信公眾號:harmonyos_developer】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
ThreadLocal實(shí)例應(yīng)用
ThreadLocal的定義、用法及優(yōu)點(diǎn)
內(nèi)存泄漏定位該如何去實(shí)現(xiàn)呢
ThreadLocal發(fā)生內(nèi)存泄漏的原因
內(nèi)存泄漏的特點(diǎn)和類型
內(nèi)存泄漏問題原理及檢視方法
如何避免內(nèi)存泄漏的方法和原則
什么是內(nèi)存泄漏?內(nèi)存泄漏有哪些現(xiàn)象
ThreadLocal基本內(nèi)容與用法
什么是內(nèi)存泄漏?如何避免JavaScript內(nèi)存泄漏
內(nèi)存泄漏會產(chǎn)生哪些后果
如何使用ThreadLocal來避免內(nèi)存泄漏
評論