ThreadLocal 為什么會(huì)內(nèi)存泄漏?如何解決?
最近,朋友小王在參加某大廠的社招面試,面試官笑瞇瞇地問:“說說ThreadLocal的作用?有啥缺點(diǎn)?”
小王心里一喜,這可是老生常談的問題,于是滔滔不絕地講了一通,啥線程隔離、啥存儲(chǔ)上下文信息、啥用戶Session,統(tǒng)統(tǒng)都擺上了臺(tái)面。
面試官聽完點(diǎn)點(diǎn)頭,接著拋出一個(gè)靈魂拷問:“那你能分析一下ThreadLocal的內(nèi)存泄漏問題嗎?以及如何避免?”
小王:“呃……這……ThreadLocal還能內(nèi)存泄漏?”
完了!涼涼!
回來后,小王苦哈哈地跟我吐槽,我趕緊給他補(bǔ)了一課。今天,我們就一起來探究:ThreadLocal的內(nèi)存泄漏問題及其解決方案!
ThreadLocal 的底層實(shí)現(xiàn)
ThreadLocal是 Java 提供的一種線程封閉機(jī)制,每個(gè)線程都可以存儲(chǔ)自己的變量副本,互不干擾。那么問題來了:這些變量存儲(chǔ)在哪里呢?
其實(shí),它們存儲(chǔ)在 Thread 里,每個(gè) Thread 內(nèi)部都有一個(gè) ThreadLocalMap,專門用來存儲(chǔ) ThreadLocal 變量。
我們來看 Thread 類的源碼(JDK 8):
圖片
嗯,這個(gè) threadLocals 變量就是核心,它的類型是 ThreadLocalMap,專門用來存儲(chǔ) ThreadLocal 的值。
再看看 ThreadLocalMap 的內(nèi)部結(jié)構(gòu)(簡(jiǎn)化版):
圖片
我們發(fā)現(xiàn)了一個(gè)關(guān)鍵點(diǎn):Entry 繼承自 WeakReference(弱引用)。這意味著 ThreadLocal 本身是弱引用,但 value 卻是強(qiáng)引用!
那么問題來了!
為什么會(huì)內(nèi)存泄漏?
我們來看這樣一段代碼:
圖片
這段代碼有兩個(gè)問題:
- threadLocal 被置為 null,但 value 依然存在!
- ThreadLocal 是弱引用,GC 可能會(huì)回收它,但 value 依然被 ThreadLocalMap 強(qiáng)引用著!
當(dāng) GC 發(fā)生時(shí):
- ThreadLocal 變量本身會(huì)被清理掉,因?yàn)樗侨跻谩?/li>
- ThreadLocalMap 的 Entry.key == null,但 value 還在,占據(jù)大量內(nèi)存!
這樣,如果當(dāng)前線程是線程池的線程,那么這個(gè) value 就一直不會(huì)被回收,導(dǎo)致內(nèi)存泄漏!
這,就是 ThreadLocal 內(nèi)存泄漏 的真正原因!
如何避免 ThreadLocal 內(nèi)存泄漏?
既然知道了原因,那解決方案也就呼之欲出了!
方案 1:手動(dòng) remove()
最簡(jiǎn)單、最有效的方式,就是在使用完 ThreadLocal 變量后,手動(dòng)調(diào)用 remove() 方法。
圖片
這樣,ThreadLocalMap 里的 Entry 就會(huì)被清理掉,value 也就不會(huì)泄漏了!
正確示例:
圖片
為什么要用 finally?
因?yàn)槿绻l(fā)生異常,導(dǎo)致 remove() 沒有執(zhí)行,那么 value 還是會(huì)泄漏!所以,我們一定要在 finally 代碼塊里手動(dòng)清理。
方案 2:使用 Static 變量避免多個(gè) ThreadLocal 實(shí)例
有時(shí)候,我們不希望 ThreadLocal 被 GC 過早回收,可以使用 static 變量 來持有它,確保 ThreadLocal 不會(huì)被回收:
圖片
不過,這種方式只適用于 ThreadLocal生命周期和應(yīng)用一致 的情況,否則可能會(huì)導(dǎo)致 ThreadLocal 變量不被回收,反而導(dǎo)致 OOM!
方案 3:使用 InheritableThreadLocal
如果是 子線程需要繼承父線程的 ThreadLocal 變量,可以使用 InheritableThreadLocal,避免子線程訪問不到 ThreadLocal 變量:
圖片
但它不能解決內(nèi)存泄漏問題,只是拓展了 ThreadLocal 的作用范圍。
總結(jié)
常見錯(cuò)誤
- 忘記 remove(),導(dǎo)致 value 無法回收。
- ThreadLocal 被回收,但 value 還在,導(dǎo)致內(nèi)存泄漏。
- 線程池使用 ThreadLocal,但不清理,導(dǎo)致長期占用內(nèi)存。
正確做法
- 在 finally 代碼塊里手動(dòng)調(diào)用 remove(),避免內(nèi)存泄漏。
- 避免不必要的 ThreadLocal 實(shí)例,盡量復(fù)用。
- 如果一定要在線程池中使用 ThreadLocal,務(wù)必 remove() 掉!
尾聲
小王看完這篇文章,恍然大悟:“原來 ThreadLocal 還有這么大的坑,難怪我面試掛了!”
“那你下次再面試,還怕被問到這個(gè)問題嗎?”我笑著問。
“怕啥!我還想主動(dòng)給面試官講一遍,順便聊聊 JVM 內(nèi)存模型!”