還記得第一次接觸到ThreadLocal可能導(dǎo)致內(nèi)存泄露的問(wèn)題是有一次面試的時(shí)候被問(wèn)到了ThreadLocal的缺陷是什么。當(dāng)然由于后來(lái)沒(méi)有面試官的聯(lián)系方式很遺憾也一直沒(méi)能確認(rèn)所謂的缺陷是不是就是可能導(dǎo)致內(nèi)存泄漏蛤售,不過(guò)后來(lái)發(fā)現(xiàn)雖然當(dāng)時(shí)弄明白了可是過(guò)段時(shí)間又搞忘記了這個(gè)問(wèn)題税朴,所以特別記錄下來(lái)做個(gè)備忘吧宛渐。
ThreadLocal從名字上來(lái)說(shuō)就很好理解,就是用于線(xiàn)程(Thread)私有(Local)的存儲(chǔ)結(jié)構(gòu),這種結(jié)構(gòu)能夠使得線(xiàn)程能夠使用只有自己能夠訪(fǎng)問(wèn)和修改的變量饿凛,從而實(shí)現(xiàn)多個(gè)線(xiàn)程之間的資源互相隔離只泼,達(dá)到安全并發(fā)的目的剖笙。
也因此,ThreadLocal作為線(xiàn)程并發(fā)中的一種資源使用方式请唱,得到了很廣泛的應(yīng)用弥咪,比如Spring MVC、Hibernate等十绑。
不過(guò)值得一提的是聚至,通常有人會(huì)講ThreadLocal和synchronised等放在一起,作為形成安全并發(fā)的手段之一本橙。其實(shí)我覺(jué)得這是比較容易使人誤導(dǎo)的扳躬,因?yàn)閮烧叩哪康男酝耆灰粯印?br>
ThreadLocal主要的是用于獨(dú)享自己的變量,避免一些資源的爭(zhēng)奪甚亭,從而實(shí)現(xiàn)了空間換時(shí)間的思想贷币。
而synchronised則主要用于臨界(沖突)資源的分配,從而能夠?qū)崿F(xiàn)線(xiàn)程間信息同步亏狰,公共資源共享等役纹,所以嚴(yán)格來(lái)說(shuō)synchronised其實(shí)是能夠?qū)崿F(xiàn)ThreadLocal所需要的達(dá)到的效果的,只不過(guò)這樣會(huì)帶來(lái)資源爭(zhēng)奪導(dǎo)致并發(fā)性能下降暇唾,而且還有synchronised促脉、線(xiàn)程切換等一些可能不必要的開(kāi)銷(xiāo)。
對(duì)于ThreadLocal而言策州,其實(shí)使用起來(lái)有點(diǎn)像基礎(chǔ)類(lèi)型的裝箱類(lèi)型的感覺(jué)(個(gè)人覺(jué)得其實(shí)也可以算是一種裝飾器模式的使用嘲叔?),具體的使用就不在啰嗦了抽活。下面就看看這次備忘的重點(diǎn)硫戈,如何導(dǎo)致內(nèi)存泄漏的。
其實(shí)網(wǎng)上有的文章已經(jīng)講的聽(tīng)清楚的下硕,覺(jué)得有張圖特別好先引用到這里丁逝,來(lái)源于ThreadLocal可能引起的內(nèi)存泄露:
所以簡(jiǎn)單的說(shuō),主要原因就是在于TreadLocal中用到的自己定義的Map(和常用的Map接口不同)中梭姓,使用的Key值是一個(gè)WeakReference類(lèi)型的值(弱引用會(huì)在下一次GC時(shí)馬上釋放而不管是否被引用)霜幼。那么如果這個(gè)Key在GC時(shí)被釋放了,就會(huì)導(dǎo)致Value永遠(yuǎn)都不會(huì)被調(diào)用到誉尖,但是如果線(xiàn)程不結(jié)束罪既,又一直存在。
因?yàn)榭赡懿皇煜み@部分內(nèi)容的同學(xué)(例如幾周以后的我)會(huì)感覺(jué)有點(diǎn)迷糊為什么這個(gè)圖是這樣的,就具體再解釋一下細(xì)節(jié)點(diǎn):
- 首先當(dāng)然是看一下我們的主角ThreadLocal類(lèi)琢感,只保留了幾個(gè)重點(diǎn)的地方丢间,特別的是內(nèi)部靜態(tài)類(lèi)的ThreadLocalMap是ThreadLocal自己實(shí)現(xiàn)的一個(gè)Map,而這個(gè)Map用使用了ThreadLocal作為了一個(gè)弱引用的Key(也就是主要問(wèn)題點(diǎn))驹针。
p.s.不知道各位第一次看的時(shí)候會(huì)不會(huì)跟我一樣有種我是老子的兒子的同時(shí)又是老子的老子的感覺(jué)烘挫,哈哈哈
public class ThreadLocal<T> {
// 獲取Thread里面的Map
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// (敲黑板)
// 這里是重點(diǎn)!<砩饮六!
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;
}
}
...
}
...
}
- 接著不得不說(shuō)的就是我們的大佬Thread類(lèi),里面關(guān)于ThreadLocal部分的內(nèi)容主要是這樣滴臂外。我們可以看到這里主要是聲明了ThreadLocal里面的Map作為類(lèi)變量來(lái)提供給線(xiàn)程使用的虽风。也正式因?yàn)槿绱耍艜?huì)在ThreadLocal里面的getMap方法是拉取的Thread里面的Map寄月。
p.s. 感覺(jué)確實(shí)有點(diǎn)繞
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- 于是到這里我們就明白了,其實(shí)每個(gè)Thread里面都有一個(gè)Map无牵,Map里面的Key是ThreadLocal類(lèi)的一個(gè)實(shí)例漾肮,之所以會(huì)比較混淆主要還是因?yàn)檫@里的Map又是ThreadLocal里面的一個(gè)內(nèi)部靜態(tài)類(lèi)。
所以到這里其實(shí)有兩個(gè)問(wèn)題是暫時(shí)還沒(méi)想通的茎毁,也希望有各位大佬指點(diǎn)一二:
- TreadLocalMap 其實(shí)是可以抽取成單獨(dú)的類(lèi)的克懊?這樣就使得邏輯和嵌套關(guān)系沒(méi)有這么繞的感覺(jué)。
- 為什么只有Key要設(shè)計(jì)成WeakReference而不是Key和Value都是七蜘,或者這里為什么要設(shè)置弱引用谭溉?如果為了保護(hù)內(nèi)存空間其實(shí)兩者都是弱引用更好吧,是不是有什么其它考慮橡卤?
回歸到內(nèi)存泄露是因?yàn)閃eakReference Key的問(wèn)題扮念,當(dāng)然,Java的各位大佬肯定早就想到這個(gè)問(wèn)題了碧库,可以看到人家注釋里面是這么說(shuō)的柜与,大意就是如果key==null的時(shí)候,就可以認(rèn)為這個(gè)值無(wú)效了嵌灰,可以調(diào)用expunged進(jìn)行清理:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
而這個(gè)expungeStaleEntry方法在get弄匕、set時(shí)都會(huì)有間接的調(diào)用,而且remove方法中也會(huì)顯示的調(diào)用沽瞭,這也就是為什么有的文章中說(shuō)通過(guò)在線(xiàn)程調(diào)用完成之后迁匠,通過(guò)調(diào)用remove方法能有效的杜絕該泄露問(wèn)題的原因。
當(dāng)然簡(jiǎn)單來(lái)說(shuō)理解到這里就基本明了內(nèi)存泄露的原因,但是其實(shí)再深入一點(diǎn)來(lái)說(shuō)城丧,如果泄露的原因是Key被釋放延曙,而Value沒(méi)有釋放,那么是否一定會(huì)有泄露呢芙贫?
答案當(dāng)然是否定的搂鲫,因?yàn)槿绻且话愕木€(xiàn)程場(chǎng)景中,除了會(huì)調(diào)用expungeStaleEntry來(lái)進(jìn)行清理磺平,最差魂仍,在線(xiàn)程結(jié)束之時(shí),自然也就消除了引用從而使得Value得以GC回收拣挪。
所以擦酌,會(huì)不會(huì)有線(xiàn)程一直不結(jié)束的場(chǎng)景呢?
當(dāng)然答案是肯定的菠劝,最簡(jiǎn)單來(lái)說(shuō)線(xiàn)程只要一直在wait就不會(huì)結(jié)束了赊舶,不過(guò)這種場(chǎng)景下其實(shí)和泄露也沒(méi)啥關(guān)系的感覺(jué)。
其實(shí)最常用的線(xiàn)程一直不結(jié)束的場(chǎng)景赶诊,自然就是線(xiàn)程池了笼平。因?yàn)檫@種情況下,線(xiàn)程是一直在不斷的重復(fù)運(yùn)行的舔痪,從而也就造成了value可能造成累積的情況寓调。具體的模擬可以參考: 深入理解ThreadLocal的"內(nèi)存溢出"
最后來(lái)做個(gè)總結(jié)吧,可能泄露的場(chǎng)景僅且僅在:
- 線(xiàn)程run方法結(jié)束后沒(méi)有顯示的調(diào)用remove進(jìn)行清理
- 線(xiàn)程在線(xiàn)程池的模式下锄码,一直重復(fù)運(yùn)行