ThreadLocal原理介紹及應(yīng)用場(chǎng)景
本次給大家介紹重要的工具ThreadLocal
。講解內(nèi)容如下,同時(shí)介紹什么場(chǎng)景下發(fā)生內(nèi)存泄漏,如何復(fù)現(xiàn)內(nèi)存泄漏,如何正確使用它來(lái)避免內(nèi)存泄漏。
ThreadLocal
是什么?有哪些用途?ThreadLocal
如何使用ThreadLocal
原理ThreadLocal
使用有哪些坑及注意事項(xiàng)
1. ThreadLocal是什么?有哪些用途?
首先介紹Thread
類中屬性threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我們發(fā)現(xiàn)Thread并沒(méi)有提供成員變量threadLocals的設(shè)置與訪問(wèn)的方法,那么每個(gè)線程的實(shí)例threadLocals參數(shù)我們?nèi)绾尾僮髂兀窟@時(shí)我們的主角:ThreadLocal就登場(chǎng)了。
所以有那么一句總結(jié):ThreadLocal
是線程Thread中
屬性threadLocals的管理者。
也就是說(shuō)我們對(duì)于ThreadLocal
的get, set,remove的操作結(jié)果都是針對(duì)當(dāng)前線程Thread實(shí)例的threadLocals存,取,刪除操作。類似于一個(gè)開(kāi)發(fā)者的任務(wù),產(chǎn)品經(jīng)理左右不了,產(chǎn)品經(jīng)理只能通過(guò)技術(shù)leader來(lái)給開(kāi)發(fā)者分配任務(wù)。下面再舉個(gè)栗子,進(jìn)一步說(shuō)明他們之間的關(guān)系:
1.每個(gè)人都一張銀行卡
2.每個(gè)人每張卡都有一定的余額。
3.每個(gè)人獲取銀行卡余額都必須通過(guò)該銀行的管理系統(tǒng)。
4.每個(gè)人都只能獲取自己卡持有的余額信息,他人的不可訪問(wèn)。
映射到我們要說(shuō)的ThreadLocal
- 1.card類似于Thread
- 2.card余額屬性,卡號(hào)屬性等類似于Treadlocal內(nèi)部屬性集合threadLocals
- 3.cardManager類似于ThreadLocal管理類
那ThreadLocal有哪些應(yīng)用場(chǎng)景呢?
其實(shí)我們無(wú)意間已經(jīng)時(shí)時(shí)刻刻在使用ThreadLocal提供的便利,如果說(shuō)多數(shù)據(jù)源的切換你比較陌生,那么spring提供的聲明式事務(wù)就再熟悉不過(guò)了,我們?cè)谘邪l(fā)過(guò)程中無(wú)時(shí)無(wú)刻不在使用,而spring聲明式事務(wù)的重要實(shí)現(xiàn)基礎(chǔ)就是ThreadLocal,只不過(guò)大家沒(méi)有去深入研究spring聲明式事務(wù)的實(shí)現(xiàn)機(jī)制。后面有機(jī)會(huì)我會(huì)給大家介紹spring聲明式事務(wù)的原理及實(shí)現(xiàn)機(jī)制。
原來(lái)ThreadLocal
這么強(qiáng)大,但應(yīng)用開(kāi)發(fā)者使用較少,同時(shí)有些研發(fā)人員對(duì)于ThreadLocal
內(nèi)存泄漏,等潛在問(wèn)題,不敢試用,恐怕這是對(duì)于ThreadLocal
最大的誤解,后面我們將會(huì)仔細(xì)分析,只要按照正確使用方式,就沒(méi)什么問(wèn)題。如果ThreadLocal存在問(wèn)題,豈不是spring聲明式事務(wù)是我們程序最大的潛在危險(xiǎn)嗎?
2.ThreadLocal如何使用
為了更直觀的體會(huì)ThreadLocal
的使用我們假設(shè)如下場(chǎng)景
- 1.我們給每個(gè)線程生成一個(gè)ID。
- 2.一旦設(shè)置,線程生命周期內(nèi)不可變化。
- 3.容器活動(dòng)期間不可以生成重復(fù)的ID
我們創(chuàng)建一個(gè)ThreadLocal管理類:
測(cè)試程序如下:我們同一個(gè)線程不斷get,測(cè)試id是否變化,同時(shí)測(cè)試完成后我們就將其釋放掉。
在主程序中我們開(kāi)啟多個(gè)線程測(cè)試不通線程之間是否會(huì)影響
不出意外我們的結(jié)果為:
結(jié)果:確實(shí)是不同線程間id不同,相同線程id相同。
3.ThreadLocal原理
①ThreadLocal類結(jié)構(gòu)及方法解析:
上圖可知:ThreadLocal
三個(gè)方法get, set , remove以及內(nèi)部類`ThreadLocalMap
②ThreadLocal及Thread之間的關(guān)系:
從這張圖我們可以直觀的看到Thread中屬性threadLocals,作為一個(gè)特殊的Map,它的key值就是我們ThreadLocal
實(shí)例,而value值這是我們?cè)O(shè)置的值。
③ThreadLocal的操作過(guò)程:
我們以get方法為例:
其中g(shù)etMap(t)返回的就上當(dāng)前線程的threadlocals,如下圖,然后根據(jù)當(dāng)前ThreadLocal實(shí)例對(duì)象作為key獲取ThreadLocalMap中的value,如果首次進(jìn)來(lái)這調(diào)用setInitialValue()
set的過(guò)程也類似:
注意:ThreadLocal
中可以直接t.threadLocals
是因?yàn)?code>Thread與ThreadLocal
在同一個(gè)包下,同樣Thread可以直接訪問(wèn)ThreadLocal.ThreadLocalMap threadLocals = null;
來(lái)進(jìn)行聲明屬性。
4.ThreadLocal使用有哪些坑及注意事項(xiàng)
我經(jīng)常在網(wǎng)上看到駭人聽(tīng)聞的標(biāo)題,ThreadLocal
導(dǎo)致內(nèi)存泄漏,這通常讓一些剛開(kāi)始對(duì)ThreadLocal
理解不透徹的開(kāi)發(fā)者,不敢貿(mào)然使用。越不用,越陌生。這樣就讓我們錯(cuò)失了更好的實(shí)現(xiàn)方案,所以敢于引入新技術(shù),敢于踩坑,才能不斷進(jìn)步。
我們來(lái)看下為什么說(shuō)ThreadLocal
會(huì)引起內(nèi)存泄漏,什么場(chǎng)景下會(huì)導(dǎo)致內(nèi)存泄漏?
先回顧下什么叫內(nèi)存泄漏,對(duì)應(yīng)的什么叫內(nèi)存溢出
- ①M(fèi)emory overflow:內(nèi)存溢出,沒(méi)有足夠的內(nèi)存提供申請(qǐng)者使用。
- ②Memory leak:內(nèi)存泄漏,程序申請(qǐng)內(nèi)存后,無(wú)法釋放已申請(qǐng)的內(nèi)存空間,內(nèi)存泄漏的堆積終將導(dǎo)致內(nèi)存溢出。
顯然是TreadLocal在不規(guī)范使用的情況下導(dǎo)致了內(nèi)存沒(méi)有釋放。
紅框里我們看到了一個(gè)特殊的類WeakReference
,同樣這個(gè)類,應(yīng)用開(kāi)發(fā)者也同樣很少使用,這里簡(jiǎn)單介紹下吧
類型 | 回收時(shí)間 | 應(yīng)用場(chǎng)景 |
---|---|---|
強(qiáng)引用 | 一直存活,除非GC Roots不可達(dá) | 所有程序的場(chǎng)景,基本對(duì)象,自定義對(duì)象等 |
軟引用 | 內(nèi)存不足時(shí)會(huì)被回收 | 一般用在對(duì)內(nèi)存非常敏感的資源上,用作緩存的場(chǎng)景比較多,例如:網(wǎng)頁(yè)緩存、圖片緩存 |
弱引用 | 只能存活到下一次GC前 | 生命周期很短的對(duì)象,例如ThreadLocal中的Key。 |
虛引用 | 隨時(shí)會(huì)被回收, 創(chuàng)建了可能很快就會(huì)被回收 | 可能被JVM團(tuán)隊(duì)內(nèi)部用來(lái)跟蹤JVM的垃圾回收活動(dòng) |
既然WeakReference
在下一次gc即將被回收,那么我們的程序?yàn)槭裁礇](méi)有出問(wèn)題呢?
- ①所以我們測(cè)試下弱引用的回收機(jī)制:
這一種存在強(qiáng)引用不會(huì)被回收。
這里沒(méi)有強(qiáng)引用將會(huì)被回收。
上面演示了弱引用的回收情況,下面我們看下ThreadLocal的弱引用回收情況。
- ②
ThreadLocal
的弱引用回收情況
如上圖所示,我們?cè)谧鳛閗ey的ThreadLocal對(duì)象沒(méi)有外部強(qiáng)引用,下一次gc必將產(chǎn)生key值為null的數(shù)據(jù),若線程沒(méi)有及時(shí)結(jié)束必然出現(xiàn),一條強(qiáng)引用鏈
Threadref–>Thread–>ThreadLocalMap–>Entry,所以這將導(dǎo)致內(nèi)存泄漏。
下面我們模擬復(fù)現(xiàn)ThreadLocal導(dǎo)致內(nèi)存泄漏:
1.為了效果更佳明顯我們將我們的treadlocals的存儲(chǔ)值value設(shè)置為1萬(wàn)字符串的列表:
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(); } }
測(cè)試代碼如下:
public static void main(String[] args) throws InterruptedException { // 為了復(fù)現(xiàn)key被回收的場(chǎng)景,我們使用臨時(shí)變量 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ā)垃圾回收,但是可以通過(guò)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) { } }
此時(shí)我們?nèi)绾沃纼?nèi)存中存在memory leak呢?
我們可以借助jdk提供的一些命令dump當(dāng)前堆內(nèi)存,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>
然后我們借助MAT可視化分析工具,來(lái)查看對(duì)內(nèi)存,分析對(duì)象實(shí)例的存活狀態(tài):
首先打開(kāi)我們工具提示我們的內(nèi)存泄漏分析:
這里我們可以確定的是ThreadLocalMap實(shí)例的Entry.value是沒(méi)有被回收的。
最后我們要確定Entry.key是否還在?打開(kāi)Dominator Tree,搜索我們的ThreadLocalMemory,發(fā)現(xiàn)并沒(méi)有存活的實(shí)例。
以上我們復(fù)現(xiàn)了ThreadLocal
不正當(dāng)使用,引起的內(nèi)存泄漏。demo在這里。
所以我們總結(jié)了使用ThreadLocal
時(shí)會(huì)發(fā)生內(nèi)存泄漏的前提條件:
- ①
ThreadLocal
引用被設(shè)置為null,且后面沒(méi)有set,get,remove操作。 - ②線程一直運(yùn)行,不停止。(線程池)
- ③觸發(fā)了垃圾回收。(Minor GC或Full GC)
我們看到ThreadLocal
出現(xiàn)內(nèi)存泄漏條件還是很苛刻的,所以我們只要破壞其中一個(gè)條件就可以避免內(nèi)存泄漏,單但為了更好的避免這種情況的發(fā)生我們使用ThreadLocal時(shí)遵守以下兩個(gè)小原則:
- ①ThreadLocal申明為private static final。
Private與final 盡可能不讓他人修改變更引用,
Static 表示為類屬性,只有在程序結(jié)束才會(huì)被回收。 - ②ThreadLocal使用后務(wù)必調(diào)用remove方法。
最簡(jiǎn)單有效的方法是使用后將其移除。
到此這篇關(guān)于ThreadLocal原理介紹及應(yīng)用場(chǎng)景的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
spring cloud gateway全局過(guò)濾器實(shí)現(xiàn)向request header中放數(shù)據(jù)
這篇文章主要介紹了spring cloud gateway全局過(guò)濾器實(shí)現(xiàn)向request header中放數(shù)據(jù)的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07IDEA中已配置阿里鏡像但maven無(wú)法下載jar包的問(wèn)題及解決方法
這篇文章主要介紹了IDEA中已配置阿里鏡像但maven無(wú)法下載jar包的問(wèn)題,本文給大家分享解決方法,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08基于swagger參數(shù)與實(shí)體中參數(shù)不一致的原因分析
這篇文章主要介紹了基于swagger參數(shù)與實(shí)體中參數(shù)不一致的原因分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11通過(guò)Java實(shí)現(xiàn)RSA加密與驗(yàn)證的方法詳解
RSA是一種非對(duì)稱加密算法,是目前廣泛應(yīng)用于加密和數(shù)字簽名領(lǐng)域的一種加密算法,本文主要講述如何通過(guò)Java實(shí)現(xiàn)RSA加密與驗(yàn)證,應(yīng)用場(chǎng)景為與其他平臺(tái)對(duì)接接口時(shí),通過(guò)RSA加密和解密驗(yàn)證請(qǐng)求的有效性,在對(duì)接時(shí)雙方互換公鑰,需要的朋友可以參考下2023-12-12Spring Data JPA 簡(jiǎn)單查詢--方法定義規(guī)則(詳解)
下面小編就為大家?guī)?lái)一篇Spring Data JPA 簡(jiǎn)單查詢--方法定義規(guī)則(詳解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04Java常用類庫(kù)Apache Commons工具類說(shuō)明及使用實(shí)例詳解
這篇文章主要介紹了Java常用類庫(kù)Apache Commons工具類說(shuō)明及使用實(shí)例詳解,需要的朋友可以參考下2020-02-02java中instanceof和getClass()的區(qū)別分析
本篇文章介紹了,在java中instanceof和getClass()的區(qū)別分析。需要的朋友參考下2013-04-04Java設(shè)計(jì)模式之抽象工廠模式AbstractFactoryPattern詳解
這篇文章主要介紹了Java設(shè)計(jì)模式之抽象工廠模式AbstractFactoryPattern詳解,抽象工廠模式是一種軟件開(kāi)發(fā)設(shè)計(jì)模式,抽象工廠模式提供了一種方式,可以將一組具有同一主題的單獨(dú)的工廠封裝起來(lái),需要的朋友可以參考下2023-10-10IntelliJ IDEA像Eclipse一樣打開(kāi)多個(gè)項(xiàng)目的圖文教程
這篇文章主要介紹了IntelliJ IDEA像Eclipse一樣打開(kāi)多個(gè)項(xiàng)目的方法圖文教程講解,需要的朋友可以參考下2018-03-03