來(lái)聊聊ThreadLocal內(nèi)存泄露分析
前幾天有個(gè)學(xué)生問(wèn)我ThreadLocal存在不存在內(nèi)存泄漏,趁此機(jī)會(huì)和大家聊聊ThreadLocal到底存在不存在內(nèi)存泄漏以及怎么避免。
Thread中的threadLocals屬性
一切都要從?Thread
?的一個(gè)屬性?threadLocals
?說(shuō)起,讓我們看下這個(gè)屬性的介紹:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
這個(gè)?threadLocals
?屬性是一個(gè)?ThreadLocal
?里的靜態(tài)類(lèi)?ThreadLocalMap
?,它是一個(gè)?map
,并且是由?ThreadLocal
?進(jìn)行維護(hù)管理的。
那么這個(gè)?threadLocals
?,也就是這個(gè)?map
?里,存的是什么呢?
我們來(lái)看?ThreadLocalMap
?:
static class ThreadLocalMap {
? ?static class Entry extends WeakReference<ThreadLocal<?>> {
? ? ? ?Object value;
? ? ? ?Entry(ThreadLocal<?> k, Object v) {
? ? ? ? ? ?super(k);
? ? ? ? ? ?value = v;
? ? ? ?}
? ?}
}
介紹里說(shuō),ThreadLocalMap
?是一個(gè)定制化的?hash map
,在?Entry
?里,以鍵值對(duì)的形式存儲(chǔ)著?ThreadLocal
?對(duì)象和?value
。
于是?Thread
?中?threadLocals
?屬性和?ThreadLocal
?的關(guān)系簡(jiǎn)圖就如下所示:
threadLocals.png
注意,這里?Entry
?里的?key
?,即?ThreadLocal
?對(duì)象是以弱引用的形式存在的,這將是本文內(nèi)存泄露分析的重點(diǎn)之一,但這里先不談,再繼續(xù)講講?Thread
?和?ThreadLocal
?的關(guān)系。
Thread和ThreadLocal的關(guān)系
先上一段示例代碼:
public class ThreadLocalDemo {
? ?private static ThreadLocal<Weapon> weaponThreadLocal = new ThreadLocal<Weapon>() {
? ? ? ?@Override
? ? ? ?protected Weapon initialValue() {
? ? ? ? ? ?return new Weapon();
? ? ? ?}
? ?};
? ?private static class Player extends Thread {
? ? ? ?@Override
? ? ? ?public void run() {
? ? ? ? ? ?weaponThreadLocal.get().level += ThreadLocalRandom.current().nextInt(5);
? ? ? ? ? ?System.out.println(getName() + " level: " + weaponThreadLocal.get().level);
? ? ? ? ? ?weaponThreadLocal.get().combatEff = weaponThreadLocal.get().level * 10;
? ? ? ? ? ?System.out.println(getName() + " combatEff: " + weaponThreadLocal.get().combatEff);
? ? ? ?}
? ?}
? ?public static void main(String[] args) {
? ? ? ?Player player1 = new Player();
? ? ? ?Player player2 = new Player();
? ? ? ?player1.start();
? ? ? ?player2.start();
? ?}
? ?private static class Weapon {
? ? ? ?int level;
? ? ? ?int combatEff;
? ? ? ?public Weapon() {
? ? ? ? ? ?level = 1;
? ? ? ? ? ?combatEff = 10;
? ? ? ?}
? ?}
}
在上述代碼中,有兩個(gè)?Player
?,他們進(jìn)行一場(chǎng)游戲,每個(gè)人在游戲開(kāi)始時(shí)都會(huì)有一把武器?Weapon
?。這把武器在游戲開(kāi)始時(shí)對(duì)每個(gè)人來(lái)說(shuō)是公平的,它的等級(jí)(level
)和戰(zhàn)斗力 (Combat Effectiveness
)都是一個(gè)固定值(由?ThreadLocal
?初始化)。隨著游戲的進(jìn)行,他們的武器等級(jí)會(huì)升級(jí),戰(zhàn)斗力會(huì)變強(qiáng),但升多少級(jí)、變強(qiáng)多少就看造化了(由?ThreadLocalRandom
?產(chǎn)生隨機(jī)數(shù))
看看運(yùn)行之后的結(jié)果吧:
Thread-1 level: 3
Thread-0 level: 5
Thread-1 combatEff: 30
Thread-0 combatEff: 50
看來(lái)線(xiàn)程0運(yùn)氣更好一點(diǎn)。
好了,上述的例子只是為了接下來(lái)的說(shuō)明做一個(gè)鋪墊,下面就從上述例子開(kāi)始談?wù)?Thread
?和?ThreadLocal
?的關(guān)系。
一般來(lái)說(shuō),可以認(rèn)為?ThreadLocal
?解決了線(xiàn)程間共享變量的問(wèn)題,即?ThreadLocal
?為每個(gè)線(xiàn)程維護(hù)了一個(gè)共享變量的副本,多個(gè)線(xiàn)程在修改這個(gè)變量時(shí)(其實(shí)是修改自己的變量副本),不存在線(xiàn)程安全問(wèn)題,效率也很高。所以,在上述例子中,對(duì)于一個(gè)共享變量,ThreadLocal
?提供了兩個(gè)功能:
統(tǒng)一設(shè)置初始值
每個(gè)線(xiàn)程對(duì)該值的修改互不影響,做到變量隔離
那么乍一看,Thread
?和?ThreadLocal
?的關(guān)系好像是這樣:

threadLocal-key-value.png
可是這樣是不對(duì)的,如果理解成這樣,我上面?Thread中的threadLocals屬性
?那一大段就白說(shuō)了。
再把那段的內(nèi)容概括下,Thread
?里有?ThreadLocalMap
,而ThreadLocal
?和?value
?以鍵值對(duì)的形式存儲(chǔ)在?ThreadLocalMap
?中,所以?Thread
?和?ThreadLocal
?的關(guān)系應(yīng)該是這樣:

thread-threadLocal.png
當(dāng)我們調(diào)用?ThreadLocal
?的?get()
、set()
?和?remove()
?操作?Thread
??對(duì)應(yīng)?的?value
?時(shí),實(shí)際上是由?Thread
?的?ThreadLocalMap
?在操作?ThreadLocal
?對(duì)應(yīng)的?value
。
對(duì)應(yīng)上述的代碼示例,如果我們?cè)俳o每個(gè)?Player
?新增一個(gè)?Life
?的共享變量,又多出一個(gè)管理?Life
?變量的?ThreadLocal
,那么它們的示意圖就該是這樣的:
add-life.png
至此,Thread
?和?ThreadLocal
?的關(guān)系應(yīng)該說(shuō)明清楚了,下面就開(kāi)始分析?ThreadLocal
?中存在的內(nèi)存泄露問(wèn)題。
ThreadLocal中內(nèi)存泄露問(wèn)題分析
要分析?ThreadLocal
?中的內(nèi)存泄露問(wèn)題,得看一張?Thread
?和?ThreadLocal
?從內(nèi)存角度分析的關(guān)系圖:

memory.png
從上圖進(jìn)行后續(xù)分析。
分析一
從上圖可知,Thread
?對(duì)象里的?threadLocals
?持有?ThreadLocalMap
?對(duì)象,Entry
?對(duì)象。那么當(dāng)線(xiàn)程執(zhí)行完畢,線(xiàn)程對(duì)象被回收,ThreadLocalMap
?也會(huì)被回收。由于?Entry
?持有?Weapon
?對(duì)象,即?value
?對(duì)象的引用,value
?對(duì)象也會(huì)被回收。除了?ThreadLocal
?對(duì)象,隨著線(xiàn)程執(zhí)行完畢,所有對(duì)象都會(huì)被回收,皆大歡喜,沒(méi)有內(nèi)存泄露。
分析二
若線(xiàn)程還在執(zhí)行中,而?ThreadLocal
?對(duì)象引用被置為?null
,即現(xiàn)在不需要?ThreadLocal
?了,那么其實(shí)?Weapon
?也失去了意義,照理說(shuō)是該把?Weapon
?對(duì)象回收的,那么怎么回收呢?
一旦?ThreadLocal
?對(duì)象引用被置為?null
,那么由于?Entry
?對(duì)象持有的是?ThreadLocal
?對(duì)象的弱引用,那么?ThreadLocal
?對(duì)象就會(huì)在下一次?YGC
?時(shí)被回收。此時(shí),Entry
?對(duì)象的?key
?為空了,value
?無(wú)法訪(fǎng)問(wèn)到了,怎么回收呢?原來(lái)對(duì)此情況早有設(shè)計(jì),當(dāng)每次在?get()
、set()
、remove()
?ThreadLocalMap
中的值的時(shí)候,都會(huì)自動(dòng)將?key
?為空的?value
?置為空,那么?value
?對(duì)象也能夠被回收了,不存在內(nèi)存泄露了。
那么內(nèi)存泄露到底存在于哪里呢?
分析三
在我們使用?ThreadLocal
?時(shí),通常是將它作為私有靜態(tài)變量使用的。如果把?ThreadLocal
?作為成員對(duì)象使用,那么每個(gè)使用的?ThreadLocal
?的類(lèi)都可能創(chuàng)建一個(gè)?ThreadLocal
?對(duì)象,而?ThreadLocal
?其實(shí)是使用?ThreadLocalMap
?對(duì)線(xiàn)程和?value
?進(jìn)行管理的,多個(gè)?ThreadLocal
?對(duì)象沒(méi)有意義,會(huì)造成內(nèi)存浪費(fèi)。
但另一方面,把?ThreadLocal
?作為靜態(tài)變量使用的話(huà),它就無(wú)法被置空了。ThreadLocal
?無(wú)法被置空,就無(wú)法通過(guò)觸發(fā)弱引用機(jī)制來(lái)回收?ThreadLocal
?對(duì)象,Entry
?里的?key
?就不會(huì)為空,就無(wú)法通過(guò)分析二的方法回收?value
?對(duì)象。
這就是內(nèi)存泄露的由來(lái)了。
總結(jié)一下內(nèi)存泄露的兩個(gè)條件:
ThreadLocal
?作為靜態(tài)變量使用線(xiàn)程未執(zhí)行完畢
在此情況下,線(xiàn)程中的?ThreadLocalMap
?中的鍵值對(duì)會(huì)越堆越多,可能產(chǎn)生內(nèi)存溢出問(wèn)題。
解決辦法
如果線(xiàn)程還在執(zhí)行,那么在?ThreadLocal
?的使命完成后,調(diào)用它的?remove()
?方法,該方法會(huì)把?Entry
?里的?key
?置空,就可以回收?value
?對(duì)象了。(這里?remove()
?方法還有待研究),但用就對(duì)了。
線(xiàn)程池臟數(shù)據(jù)分析
再分析一下?ThreadLocal
?和線(xiàn)程池一起使用時(shí)的臟數(shù)據(jù)問(wèn)題。(其實(shí)?ThreadLocal
?的內(nèi)存泄露也多數(shù)出現(xiàn)在和線(xiàn)程池一起使用的情況)
由以上分析可知,線(xiàn)程執(zhí)行完畢,線(xiàn)程對(duì)象被回收,一切問(wèn)題都不會(huì)存在。
若線(xiàn)程在線(xiàn)程池中復(fù)用,且不調(diào)用?remove
?方法,那么線(xiàn)程在執(zhí)行完畢一次任務(wù)并復(fù)用時(shí),從?ThreadLocalMap
?中取出來(lái)的?value
?就是上一次執(zhí)行任務(wù)完畢后的值。這時(shí)候,倘若我們的線(xiàn)程在執(zhí)行每次任務(wù)時(shí),沒(méi)有調(diào)用?set()
?方法對(duì)?value
?重新賦值,那么業(yè)務(wù)邏輯肯定就錯(cuò)了。
解決辦法
線(xiàn)程池復(fù)用時(shí),在線(xiàn)程的?
run()
?方法中要調(diào)用?ThreadLocal
?的?set()
?方法對(duì)?value
?重新賦值在線(xiàn)程的?
run()
?方法最后調(diào)用?ThreadLocal
?的?remove()
?方法