1. 程式人生 > >ThreadLocal記憶體洩露

ThreadLocal記憶體洩露


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進行清理
執行緒線上程池的模式下,一直重複執行