關於ThreadLocal內存泄露的備忘
<!-- 作者區域 -->
<div class="author">
<a class="avatar" href="/u/11625fc9199c">
<img src="//upload.jianshu.io/users/upload_avatars/2198180/8d290a5a8283.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/96/h/96" alt="96">
<!-- 文章內容 -->
<div data-note-content="" class="show-content">
<div class="show-content-free">
<p>還記得第一次接觸到ThreadLocal可能導致內存泄露的問題是有一次面試的時候被問到了ThreadLocal的缺陷是什麼。當然由於後來沒有面試官的聯繫方式很遺憾也一直沒能確認所謂的缺陷是不是就是可能導致內存泄漏,不過後來發現雖然當時弄明白了可是過段時間又搞忘記了這個問題,所以特別記錄下來做個備忘吧。</p>
ThreadLocal從名字上來說就很好理解,就是用於線程(Thread)私有(Local)的存儲結構,這種結構能夠使得線程能夠使用只有自己能夠訪問和修改的變量,從而實現多個線程之間的資源互相隔離,達到安全併發的目的。
也因此,ThreadLocal作爲線程併發中的一種資源使用方式,得到了很廣泛的應用,比如Spring MVC、Hibernate等。
不過值得一提的是,通常有人會講ThreadLocal和synchronised等放在一起,作爲形成安全併發的手段之一。其實我覺得這是比較容易使人誤導的,因爲兩者的目的性完全不一樣。
ThreadLocal主要的是用於獨享自己的變量,避免一些資源的爭奪,從而實現了空間換時間的思想。
而synchronised則主要用於臨界(衝突)資源的分配,從而能夠實現線程間信息同步,公共資源共享等,所以嚴格來說synchronised其實是能夠實現ThreadLocal所需要的達到的效果的,只不過這樣會帶來資源爭奪導致併發性能下降,而且還有synchronised、線程切換等一些可能不必要的開銷。
對於ThreadLocal而言,其實使用起來有點像基礎類型的裝箱類型的感覺(個人覺得其實也可以算是一種裝飾器模式的使用?),具體的使用就不在囉嗦了。下面就看看這次備忘的重點,如何導致內存泄漏的。
其實網上有的文章已經講的聽清楚的,覺得有張圖特別好先引用到這裏,來源於ThreadLocal可能引起的內存泄露:
所以簡單的說,主要原因就是在於TreadLocal中用到的自己定義的Map(和常用的Map接口不同)中,使用的Key值是一個WeakReference類型的值(弱引用會在下一次GC時馬上釋放而不管是否被引用)。那麼如果這個Key在GC時被釋放了,就會導致Value永遠都不會被調用到,但是如果線程不結束,又一直存在。
因爲可能不熟悉這部分內容的同學(例如幾周以後的我)會感覺有點迷糊爲什麼這個圖是這樣的,就具體再解釋一下細節點:
- 首先當然是看一下我們的主角ThreadLocal類,只保留了幾個重點的地方,特別的是內部靜態類的ThreadLocalMap是ThreadLocal自己實現的一個Map,而這個Map用使用了ThreadLocal作爲了一個弱引用的Key(也就是主要問題點)。
p.s.不知道各位第一次看的時候會不會跟我一樣有種我是老子的兒子的同時又是老子的老子的感覺,哈哈哈
public class ThreadLocal<T> {
<span class="hljs-comment">// 獲取Thread裏面的Map</span>
<span class="hljs-function">ThreadLocalMap <span class="hljs-title">getMap</span><span class="hljs-params">(Thread t)</span> </span>{
<span class="hljs-keyword">return</span> t.threadLocals;
}
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">createMap</span><span class="hljs-params">(Thread t, T firstValue)</span> </span>{
t.threadLocals = <span class="hljs-keyword">new</span> ThreadLocalMap(<span class="hljs-keyword">this</span>, firstValue);
}
<span class="hljs-comment">// (敲黑板)</span>
<span class="hljs-comment">// 這裏是重點!!!</span>
<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ThreadLocalMap</span> {</span>
<span class="hljs-comment">// 這裏是兇器!!!</span>
<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Entry</span> <span class="hljs-title">extends</span> <span class="hljs-title">WeakReference</span><ThreadLocal<?>> {</span>
<span class="hljs-comment">/** The value associated with this ThreadLocal. */</span>
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
...
}
- 接着不得不說的就是我們的大佬Thread類,裏面關於ThreadLocal部分的內容主要是這樣滴。我們可以看到這裏主要是聲明瞭ThreadLocal裏面的Map作爲類變量來提供給線程使用的。也正式因爲如此,纔會在ThreadLocal裏面的getMap方法是拉取的Thread裏面的Map。
p.s. 感覺確實有點繞
public class Thread implements Runnable {
<span class="hljs-comment">/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class.
*/</span>
ThreadLocal.ThreadLocalMap threadLocals = <span class="hljs-keyword">null</span>;
<span class="hljs-comment">/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/</span>
ThreadLocal.ThreadLocalMap inheritableThreadLocals = <span class="hljs-keyword">null</span>;
- 於是到這裏我們就明白了,其實每個Thread裏面都有一個Map,Map裏面的Key是ThreadLocal類的一個實例,之所以會比較混淆主要還是因爲這裏的Map又是ThreadLocal裏面的一個內部靜態類。
所以到這裏其實有兩個問題是暫時還沒想通的,也希望有各位大佬指點一二:
- TreadLocalMap 其實是可以抽取成單獨的類的?這樣就使得邏輯和嵌套關係沒有這麼繞的感覺。
- 爲什麼只有Key要設計成WeakReference而不是Key和Value都是,或者這裏爲什麼要設置弱引用?如果爲了保護內存空間其實兩者都是弱引用更好吧,是不是有什麼其它考慮?
迴歸到內存泄露是因爲WeakReference Key的問題,當然,Java的各位大佬肯定早就想到這個問題了,可以看到人家註釋裏面是這麼說的,大意就是如果key==null的時候,就可以認爲這個值無效了,可以調用expunged進行清理:
/**
* 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.
*/
而這個expungeStaleEntry方法在get、set時都會有間接的調用,而且remove方法中也會顯示的調用,這也就是爲什麼有的文章中說通過在線程調用完成之後,通過調用remove方法能有效的杜絕該泄露問題的原因。
當然簡單來說理解到這裏就基本明瞭內存泄露的原因,但是其實再深入一點來說,如果泄露的原因是Key被釋放,而Value沒有釋放,那麼是否一定會有泄露呢?
答案當然是否定的,因爲如果是一般的線程場景中,除了會調用expungeStaleEntry來進行清理,最差,在線程結束之時,自然也就消除了引用從而使得Value得以GC回收。
所以,會不會有線程一直不結束的場景呢?
當然答案是肯定的,最簡單來說線程只要一直在wait就不會結束了,不過這種場景下其實和泄露也沒啥關係的感覺。
其實最常用的線程一直不結束的場景,自然就是線程池了。因爲這種情況下,線程是一直在不斷的重複運行的,從而也就造成了value可能造成累積的情況。具體的模擬可以參考: 深入理解ThreadLocal的"內存溢出"
最後來做個總結吧,可能泄露的場景僅且僅在:
- 線程run方法結束後沒有顯示的調用remove進行清理
- 線程在線程池的模式下,一直重複運行
</div>
</div>
</div>