ThreadLocal為什么會(huì)導(dǎo)致內(nèi)存泄漏?
一個(gè)線程對(duì)應(yīng)一塊工作內(nèi)存,線程可以存儲(chǔ)多個(gè)ThreadLocal。那么假設(shè),開(kāi)啟1萬(wàn)個(gè)線程,每個(gè)線程創(chuàng)建1萬(wàn)個(gè)ThreadLocal,也就是每個(gè)線程維護(hù)1萬(wàn)個(gè)ThreadLocal小內(nèi)存空間,而且當(dāng)線程執(zhí)行結(jié)束以后,假設(shè)這些ThreadLocal里的Entry還不會(huì)被回收,那么將很容易導(dǎo)致堆內(nèi)存溢出。
JVM提供解決方案是把ThreadLocal里的Entry設(shè)置為弱引用,當(dāng)垃圾回收的時(shí)候,回收ThreadLocal。
什么是弱引用?
Key使用強(qiáng)引用:也就是上述說(shuō)的情況,引用ThreadLocal的對(duì)象被回收了,ThreadLocal的引用ThreadLocalMap的Key為強(qiáng)引用并沒(méi)有被回收,如果不手動(dòng)回收的話,ThreadLocal將不會(huì)回收那么將導(dǎo)致內(nèi)存泄漏。
Key使用弱引用:引用的ThreadLocal的對(duì)象被回收了,ThreadLocal的引用ThreadLocalMap的Key為弱引用,如果內(nèi)存回收,那么將ThreadLocalMap的Key將會(huì)被回收,ThreadLocal也將被回收。value在ThreadLocalMap調(diào)用get、set、remove的時(shí)候就會(huì)被清除。
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果都沒(méi)有手動(dòng)刪除對(duì)應(yīng)key,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會(huì)內(nèi)存泄漏,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
雖然JVM有保障了,但還是有內(nèi)存泄漏風(fēng)險(xiǎn)。
ThreadLocalMap使用ThreadLocal對(duì)象作為弱引用,當(dāng)垃圾回收的時(shí)候,ThreadLocalMap中Key將會(huì)被回收,也就是將Key設(shè)置為null的Entry。如果線程遲遲無(wú)法結(jié)束,也就是ThreadLocal對(duì)象將一直不會(huì)回收,回顧到上面存在很多線程+TheradLocal,那么也將導(dǎo)致內(nèi)存泄漏。(內(nèi)存泄露的重點(diǎn))
其實(shí),在ThreadLocal中,當(dāng)調(diào)用remove、get、set方法的時(shí)候,會(huì)清除為null的弱引用,也就是回收ThreadLocal。
?ThreadLocal提供一個(gè)線程(Thread)局部變量,訪問(wèn)到某個(gè)變量的每一個(gè)線程都擁有自己的局部變量。說(shuō)白了,ThreadLocal就是想在多線程環(huán)境下去保證成員變量的安全。?

對(duì)于ThreadLocal而言,常用的方法,就是get/set/initialValue方法。
我們先來(lái)看一個(gè)例子


很顯然,在這里,并沒(méi)有通過(guò)ThreadLocal達(dá)到線程隔離的機(jī)制,可見(jiàn)ThreadLocal不是保證線程安全的
雖然,ThreadLocal讓訪問(wèn)某個(gè)變量的線程都擁有自己的局部變量,但是如果這個(gè)局部變量都指向同一個(gè)對(duì)象呢?這個(gè)時(shí)候ThreadLocal就失效了。仔細(xì)觀察下圖中的代碼,你會(huì)發(fā)現(xiàn),threadLocal在初始化時(shí)返回的都是同一個(gè)對(duì)象a!
我們直接看最常用的set操作:



你會(huì)看到,set需要首先獲得當(dāng)前線程對(duì)象Thread;
然后取出當(dāng)前線程對(duì)象的成員變量ThreadLocalMap;
如果ThreadLocalMap存在,那么進(jìn)行KEY/VALUE設(shè)置,KEY就是ThreadLocal;
如果ThreadLocalMap沒(méi)有,那么創(chuàng)建一個(gè);
說(shuō)白了,當(dāng)前線程中存在一個(gè)Map變量,KEY是ThreadLocal,VALUE是你設(shè)置的值。
看一下get操作:

這里其實(shí)揭示了ThreadLocalMap里面的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu),從上面的代碼來(lái)看,ThreadLocalMap中存放的就是Entry,Entry的KEY就是ThreadLocal,VALUE就是值。
ThreadLocalMap.Entry:

在JAVA里面,存在強(qiáng)引用、弱引用、軟引用、虛引用。這里主要談一下強(qiáng)引用和弱引用。
強(qiáng)引用,就不必說(shuō)了,類似于:
A a = new A();
B b = new B();
考慮這樣的情況:
C c = new C(b);
b = null;
考慮下GC的情況。要知道b被置為null,那么是否意味著一段時(shí)間后GC工作可以回收b所分配的內(nèi)存空間呢?答案是否定的,因?yàn)榧幢鉨被置為null,但是c仍然持有對(duì)b的引用,而且還是強(qiáng)引用,所以GC不會(huì)回收b原先所分配的空間!既不能回收利用,又不能使用,這就造成了內(nèi)存泄露。
那么如何處理呢?
可以c = null;也可以使用弱引用?。╓eakReference w = new WeakReference(b);)
分析到這里,我們可以得到:

這里我們思考一個(gè)問(wèn)題:ThreadLocal使用到了弱引用,是否意味著不會(huì)存在內(nèi)存泄露呢??
首先來(lái)說(shuō),如果把ThreadLocal置為null,那么意味著Heap中的ThreadLocal實(shí)例不在有強(qiáng)引用指向,只有弱引用存在,因此GC是可以回收這部分空間的,也就是key是可以回收的。但是value卻存在一條從Current Thread過(guò)來(lái)的強(qiáng)引用鏈。因此只有當(dāng)Current Thread銷(xiāo)毀時(shí),value才能得到釋放。
因此,只要這個(gè)線程對(duì)象被gc回收,就不會(huì)出現(xiàn)內(nèi)存泄露,但在threadLocal設(shè)為null和線程結(jié)束這段時(shí)間內(nèi)不會(huì)被回收的,就發(fā)生了我們認(rèn)為的內(nèi)存泄露。最要命的是線程對(duì)象不被回收的情況,比如使用線程池的時(shí)候,線程結(jié)束是不會(huì)銷(xiāo)毀的,再次使用的,就可能出現(xiàn)內(nèi)存泄露。
那么如何有效的避免呢?
事實(shí)上,在ThreadLocalMap中的set/getEntry方法中,會(huì)對(duì)key為null(也即是ThreadLocal為null)進(jìn)行判斷,如果為null的話,那么是會(huì)對(duì)value置為null的。我們也可以通過(guò)調(diào)用ThreadLocal的remove方法進(jìn)行釋放!