ThreadLocal記憶體洩露
阿新 • • 發佈:2018-12-09
ThreadLocal從名字上來說就很好理解,就是用於執行緒(Thread)私有(Local)的儲存結構, 這種結構能夠使得執行緒能夠使用只有自己能夠訪問和修改的變數, 從而實現多個執行緒之間的資源互相隔離,達到安全併發的目的。 也因此,ThreadLocal作為執行緒併發中的一種資源使用方式,得到了很廣泛的應用,比如Spring MVC、 Hibernate等。 不過值得一提的是,通常有人會講ThreadLocal和synchronised等放在一起,作為形成安全併發的手段之一。 其實我覺得這是比較容易使人誤導的,因為兩者的目的性完全不一樣。 ThreadLocal主要的是用於獨享自己的變數,避免一些資源的爭奪,從而實現了空間換時間的思想。 而synchronised則主要用於臨界(衝突)資源的分配,從而能夠實現執行緒間資訊同步,公共資源共享等, 所以嚴格來說synchronised其實是能夠實現ThreadLocal所需要的達到的效果的, 只不過這樣會帶來資源爭奪導致併發效能下降,而且還有synchronised、執行緒切換等一些可能不必要的開銷。 對於ThreadLocal而言,其實使用起來有點像基礎型別的裝箱型別的感覺(個人覺得其實也可以算是一種裝飾器 模式的使用?),具體的使用就不在囉嗦了。下面就看看這次備忘的重點,如何導致記憶體洩漏的。 其實網上有的文章已經講的聽清楚的,覺得有張圖特別好先引用到這裡,來源於ThreadLocal可能引起的記憶體洩露: 作者:朱端的一坨 連結:https://www.jianshu.com/p/250798f9ff76
所以簡單的說,主要原因就是在於TreadLocal中用到的自己定義的Map(和常用的Map介面不同)中, 使用的Key值是一個WeakReference型別的值(弱引用會在下一次GC時馬上釋放而不管是否被引用)。 那麼如果這個Key在GC時被釋放了,就會導致Value永遠都不會被呼叫到,但是如果執行緒不結束,又一直存在。 因為可能不熟悉這部分內容的同學(例如幾周以後的我)會感覺有點迷糊為什麼這個圖是這樣的,就具體再解釋一 下細節點: 首先當然是看一下我們的主角ThreadLocal類,只保留了幾個重點的地方,特別的是內部靜態類的 ThreadLocalMap是ThreadLocal自己實現的一個Map,而這個Map用使用了ThreadLocal作為了一個弱引用的 Key(也就是主要問題點) 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); } // (敲黑板) // 這裡是重點!!! 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; } } ... } ... } 接著不得不說的就是我們的大佬Thread類,裡面關於ThreadLocal部分的內容主要是這樣滴 。我們可以看到這裡主要是聲明瞭ThreadLocal裡面的Map作為類變數來提供給執行緒使用的。 也正式因為如此,才會在ThreadLocal裡面的getMap方法是拉取的Thread裡面的Map。 p.s. 感覺確實有點繞 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; 於是到這裡我們就明白了,其實每個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可能造成累積的情況 最後來做個總結 可能洩露的場景僅且僅在: 執行緒run方法結束後沒有顯示的呼叫remove進行清理 執行緒線上程池的模式下,一直重複執行