java中ThreadLocal的基本原理
源碼實(shí)現(xiàn)
一個(gè)線程內(nèi)可以存多個(gè)ThreadLocal對象,存儲的位置位于Thread的ThreadLocal.ThreadLocalMap變量,在Thread中有如下變量:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是由ThreadLocal維護(hù)的靜態(tài)內(nèi)部類,正如代碼中注解所說這個(gè)變量是由ThreadLocal維護(hù)的。
基本流程
ThreadLoalMap數(shù)據(jù)結(jié)構(gòu)
ThreadLoalMap是ThreadLocal中的一個(gè)靜態(tài)內(nèi)部類,類似HashMap的數(shù)據(jù)結(jié)構(gòu),但并沒有實(shí)現(xiàn)Map接口。
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; // ... }
ThreadLoalMap中初始化了一個(gè)大小16的Entry數(shù)組,Entry對象用來保存每一個(gè)key-value鍵值對。通過上面的set方法,我們已經(jīng)知道其中的key永遠(yuǎn)都是ThreadLocal對象。
Hash沖突及解決
Entry在table中存儲位置是通過hashcode算法獲得。
在向ThreadLocalMap中的Entry數(shù)值存儲Entry對象時(shí),會根據(jù)ThreadLocal對象的hash值,定位到table中的位置i。分三種情況:
- 如果當(dāng)前位置為空的,直接將Entry存放在對應(yīng)位置;
- 如果位置i已經(jīng)有值且這個(gè)Entry對象的key正好是即將設(shè)置的key,那么重新設(shè)置Entry中的value;
- 如果位置i的Entry對象和即將設(shè)置的key沒關(guān)系,則尋找一個(gè)空位置;
計(jì)算hash值便會有hash沖突出現(xiàn),常見的解決方法有:再哈希法、開放地址法、建立公共溢出區(qū)、鏈?zhǔn)降刂贩ǖ取?/p>
上面的流程可以看出這里采用的是開放地址方法,如果當(dāng)前位置有值,就繼續(xù)尋找下一個(gè)位置,注意table[len-1]的下一個(gè)位置是table[0],就像是一個(gè)環(huán)形數(shù)組,所以也叫閉散列法。 如果一直都找不到空位置就會出現(xiàn)死循環(huán),發(fā)生內(nèi)存溢出。當(dāng)然有擴(kuò)容機(jī)制,一般不會找不到空位置的。
ThreadLocal內(nèi)存泄露
內(nèi)存引用鏈路
根據(jù)前面對ThreadLocal的分析,得知每個(gè)Thread維護(hù)一個(gè)ThreadLocalMap,它key是ThreadLocal實(shí)例本身,value是業(yè)務(wù)需要存儲的Object。也就是說ThreadLocal本身并不存儲值,它只是作為一個(gè)key來讓線程從ThreadLocalMap獲取value。
ThreadLocalMap是使用ThreadLocal的弱引用作為Key的,弱引用的對象在GC時(shí)會被回收。因此使用了ThreadLocal后,引用鏈如圖所示:(其中虛線表示弱引用。)
引用類型
強(qiáng)引用:java默認(rèn)的引用類型,例如 Object a = new Object();其中 a 為強(qiáng)引用,new Object()為一個(gè)具體的對象。一個(gè)對象從根路徑能找到強(qiáng)引用指向它,jvm虛擬機(jī)就不會回收。
軟引用(SoftReference):進(jìn)行年輕代的垃圾回收不會觸發(fā)SoftReference所指向?qū)ο蟮幕厥眨?strong>但如果觸發(fā)Full GC,那SoftReference所指向的對象將被回收。備注:是除了軟引用之外沒有其他強(qiáng)引用引用的情況下。
弱引用(WeakReference) :如果對象除了有弱引用指向它后沒有其他強(qiáng)引用關(guān)聯(lián)它,當(dāng)進(jìn)行年輕代垃圾回收時(shí),該引用指向的對象就會被垃圾回收器回收。
虛引用(PhantomeReference) :該引用指向的對象,無法對垃圾收集器收集對象時(shí)產(chǎn)生任何影響,但在執(zhí)行垃圾回收后垃圾收集器會通過注冊在PhantomeReference上的隊(duì)列來通知應(yīng)用程序?qū)ο蟊换厥铡?/strong>
為什么使用弱引用而不是強(qiáng)引用?
問題1:從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但為什么JDK采用了弱引用的實(shí)現(xiàn)而不是強(qiáng)引用呢?
答案是:弱引用反而是為了解決內(nèi)存存儲問題而專門使用的。
問題2:如果應(yīng)用程序覺得ThreadLocal對象的使命完成,將threadLocal ref 設(shè)置為null,如果Entry中引用ThreadLocald對象的引用類型設(shè)置為強(qiáng)引用的話,會發(fā)生什么問題?
答案是:ThreadLocal對象會無法被垃圾回收器回收,因?yàn)閺膖hread對象出發(fā),有強(qiáng)引用指向ThreadLocal的object。此時(shí)會違背用戶的初衷,造成所謂的內(nèi)存泄露。
我們先來假設(shè)一下,如果key使用強(qiáng)引用,那么在其他持有ThreadLocal引用的對象都回收了,但ThreadLocalMap依舊持有ThreadLocal的強(qiáng)引用,這就導(dǎo)致ThreadLocal不會被回收,從而導(dǎo)致Entry內(nèi)存泄露。
對照一下,弱引用的情況。持有ThreadLocal引用的對象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,會被自動回收。只不過對應(yīng)的value值,需要在下次調(diào)用set/get/remove方法時(shí)會被清除。
泄露原因分析
當(dāng)Thread執(zhí)行完會被銷毀,Thread.threadLocals指向的ThreadLocalMap實(shí)例也隨之變?yōu)槔锩娲娣诺腅ntity也會被回收。這種情況是不會發(fā)生內(nèi)存泄漏的。
發(fā)生內(nèi)存泄露的場景一般存在于線程池的情況下。 此時(shí),Thread生命周期比較長(存在循環(huán)使用),threadLocals引用一直存在,當(dāng)其存放的ThreadLocal被回收(弱引用生命周期比較短)后,對應(yīng)的Entity就成了key為null的實(shí)例,但value值不會被回收。 如果此Entity一直不被get()、set()、remove(),就一直不會被回收,也就發(fā)生了內(nèi)存泄漏。
所以,通常在使用完ThreadLocal后需要調(diào)用remove()方法進(jìn)行內(nèi)存的清除。
接下來我們再延伸一下,想再來談?wù)劸W(wǎng)絡(luò)上關(guān)于ThreadLocalMap中存儲大量Entry對象導(dǎo)致的內(nèi)存“泄露”問題?
網(wǎng)絡(luò)觀點(diǎn):在使用ThreadLocal中set方法與remove方法需要成對執(zhí)行,需要沒有執(zhí)行remove方法會造成內(nèi)存泄露?甚至造成內(nèi)存溢出?
我的觀點(diǎn):當(dāng)然能成對使用當(dāng)然更好,但在實(shí)際情況中,其實(shí)不調(diào)用remove方法也不太容易造成內(nèi)存溢出,因?yàn)閺拇鎯Y(jié)構(gòu)來看,除非創(chuàng)建海量線程,并且這些線程都不釋放,導(dǎo)致大量線程內(nèi)部持有的ThreadLocalMap中對象一直不會釋放,但一個(gè)線程所持有的Entry對象個(gè)數(shù)不多,取決于關(guān)聯(lián)的ThreadLocal對象個(gè)數(shù),故我們需要的關(guān)注點(diǎn)而不是remove方法,而是防止線程資源泄露。
ThreadLocal應(yīng)用場景
- 線程間數(shù)據(jù)隔離,各線程的ThreadLocal互不影響;
- 方便同一個(gè)線程使用某一對象,避免不必要的參數(shù)傳遞;
- 全鏈路追蹤中的traceId或者流程引擎中上下文的傳遞一般采用ThreadLocal;
- Spring事務(wù)管理器采用了ThreadLocal;
- Spring MVC的RequestContextHolder的實(shí)現(xiàn)使用了ThreadLocal;
到此這篇關(guān)于ThreadLocal的基本原理的文章就介紹到這了,更多相關(guān)ThreadLocal原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用3個(gè)實(shí)例從原理到實(shí)戰(zhàn)講清楚Log4j史詩級漏洞
最近應(yīng)該很多人都在關(guān)注著一個(gè)漏洞Apache Log4j 2遠(yuǎn)程代碼執(zhí)行,該漏洞一旦被攻擊者利用會造成嚴(yán)重危害,這篇文章主要給大家介紹了關(guān)于如何用3個(gè)實(shí)例從原理到實(shí)戰(zhàn)講清楚Log4j史詩級漏洞的相關(guān)資料,需要的朋友可以參考下2021-12-12Java 使用 HttpClient 發(fā)送 GET請求和 POST請求
本文主要介紹了Java 使用 HttpClient 發(fā)送 GET請求和 POST請求,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08Java鍵值對Pair的使用方式和操作實(shí)現(xiàn)
鍵值對是一種常見的數(shù)據(jù)結(jié)構(gòu),它由一個(gè)唯一的鍵和與之關(guān)聯(lián)的值組成,本文就來介紹一下Java鍵值對Pair的使用方式和操作實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12Java 數(shù)組元素倒序的三種方式(小結(jié))
這篇文章主要介紹了Java 數(shù)組元素倒序的三種方式(小結(jié)),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09