ThreadLocal小結-到底會不會引起內存泄露

[TOC]

1. ThreadLocal簡介

網上看到一些文章,提到關于ThreadLocal可能引起的內存泄露,搞得都不敢在代碼里隨意使用了,于是來研究下,看看到底ThreadLocal會不會導致內存泄露,什么情況下會導致泄露。

ThreadLocal,顧名思義,其存儲的內容是線程私本地的/私有的,我們常使用ThreadLocal來存儲/維護一些資源或者變量,以避免線程爭用或者同步問題,例如使用ThreadLocal來為每個線程維持一個redis連接(生產中這也許不是一個好的方式,還是推薦專業的連接池)或者維持一些線程私有的變量等。

例如,假設我們在一個線程應用中需要對時間做格式化,我們很容易想到的是使用SimpleDateFormat這個工具類,但是SimpleDateFormat不是線程安全的,那么我們通常用兩種做法:

  • 每次用到的時候new一個SimpleDateFormat對象,使用完丟棄,交給gc
  • 每個線程維護一個SimpleDateFormat實例,線程運行期間不重復創建

那么無論從執行效率還是內存占用方面,我們都傾向于使用后者,即線程私有一個SimpleDateFormat對象,這時候,ThreadLocal就是很好的應用,示例代碼如下:

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestTask implements Runnable {
    private boolean stop = false;
    private ThreadLocal<SimpleDateFormat> sdfHolder = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    @Override
    public void run() {
        while (!stop) {
            String formatedDateStr = sdfHolder.get().format(new Date());
            System.out.println("formated date str:" + formatedDateStr);
        //may be sleep for a while to avoid high cpu cost
        }
        sdfHolder.remove();
    }
    
    //something else
}

代碼中模擬了一個需要反復執行的Task,其run方法中,while條件除非stop是true,否則就一直運轉下去。在該示例中通過ThreadLocal為每個線程實例化了一個SimpleDateFormat對象,當需要的時候,通過get()獲取即可,實現了每個線程全程只有一個SimpleDateFormat對象。同時在stop為true時使用ThreadLocal的remove方法刪除當前線程使用的SimpleDateFormat對象,以便于垃圾回收。

僅演示ThreadLocal用法,暫不討論代碼設計

2. ThreadLocal內存模型

上面我們簡單介紹了ThreadLocal的概念和使用,下面看下ThreadLocal的內存模型。

2.1 ThreadLocal內存模型

2.1.1 私有變量存儲在哪里

在代碼中,我們使用ThreadLocal實例提供的set/get方法來存儲/使用value,但ThreadLocal實例其實只是一個引用,真正存儲值的是一個Map,其key實ThreadLocal實例本身,value是我們設置的值,分布在堆區。這個Map的類型是ThreadL.ThreadLocalMap(ThreadLocalMap是ThreadLocal的內部類),其key的類型是ThreadLocal,value是Object,類定義如下:

    static class ThreadLocalMap {
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
    }

那么當我們重寫init或者調用set/get的時候,內部的邏輯是怎樣的呢,按照上面的說法,應該是將value存儲到了ThreadLocalMap中,或者從已有的ThreadLocalMap中獲取value,我們來通過代碼分析一下。

ThreadLocal.set(T value)

set的邏輯比較簡單,就是獲取當前線程的ThreadLocalMap,然后往map里添加KV,K是this,也就是當前ThreadLocal實例,V是我們傳入的value。

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

其內部實現首先需要獲取關聯的Map,我們看下getMap和createMap的實現

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

可以看到,getMap就是返回了當前Thread實例的map(t.threadLocals),create也是創建了Thread的map(t.threadLocals),也就是說對于一個Thread實例,ThreadLocalMap是其內部的一個屬性,在需要的時候,可以通過ThreadLocal創建或者獲取,然后存放相應的值。我們看下Thread類的關鍵代碼

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //省略了其他代碼
}

可以看到,Thread中定義了屬性threadLocals,但其初始化和使用的過程,都是通過ThreadLocal這個類來執行的。

ThreadLocal.get()

get是獲取當前線程的對應的私有變量,是我們之前set或者通過initialValue指定的變量,其代碼如下

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

可以看到,其邏輯也比較簡單清晰:

  • 獲取當前線程的ThreadLocalMap實例
  • 如果不為空,以當前ThreadLocal實例為key獲取value
  • 如果ThreadLocalMap為空或者根據當前ThreadLocal實例獲取的value為空,則執行setInitialValue()

setInitialValue()內部如下:

  • 調用我們重寫的initialValue得到一個value
  • 將value放入到當前線程對應的ThreadLocalMap中
  • 如果map為空,先實例化一個map,然后賦值KV

關鍵設計小結

代碼分析到這里,其實對于ThreadLocal的內部主要設計以及其和Thread的關系比較清楚了:

  • 每個線程,是一個Thread實例,其內部擁有一個名為threadLocals的實例成員,其類型是ThreadLocal.ThreadLocalMap
  • 通過實例化ThreadLocal實例,我們可以對當前運行的線程設置一些線程私有的變量,通過調用ThreadLocal的set和get方法存取
  • ThreadLocal本身并不是一個容器,我們存取的value實際上存儲在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key
  • 每個線程實例都對應一個TheadLocalMap實例,我們可以在同一個線程里實例化很多個ThreadLocal來存儲很多種類型的值,這些ThreadLocal實例分別作為key,對應各自的value
  • 當調用ThreadLocal的set/get進行賦值/取值操作時,首先獲取當前線程的ThreadLocalMap實例,然后就像操作一個普通的map一樣,進行put和get

當然,這個ThreadLocalMap并不是一個普通的Map(比如常用的HashMap),而是一個特殊的,key為弱引用的map,這個我們后面再詳談

2.1.2 ThreadLocal內存模型

通過上一節的分析,其實我們已經很清楚ThreadLocal的相關設計了,對數據存儲的具體分布也會有個比較清晰的概念。下面的圖是網上找來的常見到的示意圖,我們可以通過該圖對ThreadLocal的存儲有個更加直接的印象。

TheadLocal內存模型

我們知道Thread運行時,線程的的一些局部變量和引用使用的內存屬于Stack(棧)區,而普通的對象是存儲在Heap(堆)區。根據上圖,基本分析如下:

  • 線程運行時,我們定義的TheadLocal對象被初始化,存儲在Heap,同時線程運行的棧區保存了指向該實例的引用,也就是圖中的ThreadLocalRef
  • 當ThreadLocal的set/get被調用時,虛擬機會根據當前線程的引用也就是CurrentThreadRef找到其對應在堆區的實例,然后查看其對用的TheadLocalMap實例是否被創建,如果沒有,則創建并初始化。
  • Map實例化之后,也就拿到了該ThreadLocalMap的句柄,然后如果將當前ThreadLocal對象作為key,進行存取操作
  • 圖中的虛線,表示key對ThreadLocal實例的引用是個弱引用

3. 插曲:強引用/弱引用

java中的引用分為四種,按照引用強度不同,從強到弱依次為:強引用、軟引用、弱引用和虛引用,如果不是專門做jvm研究,對其概念很難清晰的定義,我們大致可以理解為,引用的強度,代表了對內存占用的能力大小,具體體現在GC的時候,會不會被回收,什么時候被回收

ThreadLocal被用作TheadLocalMap的弱引用key,這種設計也是ThreadLocal被討論內存泄露的熱點問題,因此有必要了解一下什么是弱引用。

3.1 強引用

強引用雖然在開發過程中并不怎么提及,但是無處不在,例如我們在一個對象中通過如下代碼實例化一個StringBuffer對象

StringBuffer buffer = new StringBuffer();

我們知道StringBuffer的實例通常是被創建在堆中的,而當前對象持有該StringBuffer對象的引用,以便后續的訪問,這個引用,就是一個強引用。

對GC知識比較熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般默認是可達性算法,即在每一輪GC的時候,選定一些對象作為GC ROOT,然后以它們為根發散遍歷,遍歷完成之后,如果一個對象不被任何GC ROOT引用,那么它就是不可達對象,則在接下來的GC過程中很可能會被回收。

強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的交互。具體來說,如果一個對象通過一串強引用鏈接可到達(Strongly reachable),它是不會被回收的。如果你不想讓你正在使用的對象被回收,這就正是你所需要的。

3.2 軟引用

軟引用是用來描述一些還有用但是并非必須的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收返回之后進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。JDK1.2之后提供了SoftReference來實現軟引用。

相對于強引用,軟引用在內存充足時可能不會被回收,在內存不夠時會被回收。

3.3 弱引用

弱引用也是用來描述非必須的對象的,但它的強度更弱,被弱引用關聯的對象只能生存到下一次GC發生之前,也就是說下一次GC就會被回收。JDK1.2之后,提供了WeakReference來實現弱引用。

3.4 虛引用

虛引用也成為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個瑞祥是否有虛引用的存在,完全不會對其生存時間造成影響,也無法通過虛引用來取得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是在這個對象被GC時收到一個系統通知。JDK1.2之后提供了PhantomReference來實現虛引用

4. 可能的內存泄露分析

了解了ThreadLocal的內部模型以及弱引用,接下來可以分析一下是否有內存泄露的可能以及如何避免。

4.1 內存泄露分析

根據上一節的內存模型圖我們可以知道,由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關聯著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對象引用,則在下一次GC的時候,ThreadLocal實例就會被回收,那么此時ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會被外部訪問到,而且只要Thread實例一直存在,Thread實例就強引用著ThreadLocalMap,因此ThreadLocalMap就不會被回收,那么這里K為null的V就一直占用著內存

綜上,發生內存泄露的條件是

  • ThreadLocal實例沒有被外部強引用,比如我們假設在提交到線程池的task中實例化的ThreadLocal對象,當task結束時,ThreadLocal的強引用也就結束了
  • ThreadLocal實例被回收,但是在ThreadLocalMap中的V沒有被任何清理機制有效清理
  • 當前Thread實例一直存在,則會一直強引用著ThreadLocalMap,也就是說ThreadLocalMap也不會被GC

也就是說,如果Thread實例還在,但是ThreadLocal實例卻不在了,則ThreadLocal實例作為key所關聯的value無法被外部訪問,卻還被強引用著,因此出現了內存泄露。

也就是說,我們回答了文章開頭的第一個問題,ThreadLocal如果使用的不當,是有可能引起內存泄露的,雖然觸發的場景不算很容易。

這里要額外說明一下,這里說的內存泄露,是因為對其內存模型和設計不了解,且編碼時不注意導致的內存管理失聯,而不是有意為之的一直強引用或者頻繁申請大內存。比如如果編碼時不停的人為塞一些很大的對象,而且一直持有引用最終導致OOM,不能算作ThreadLocal導致的“內存泄露”,只是代碼寫的不當而已!

4.2 TheadLocal本身的優化

進一步分析ThreadLocalMap的代碼,可以發現ThreadLocalMap內部也是做了一定的優化的

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

可以看到,在set值的時候,有一定的幾率會執行replaceStaleEntry(key, value, i)方法,其作用就是將當前的值替換掉以前的key為null的值,重復利用了空間。

5. ThreadLocal使用建議

通過前面幾節的分析,我們基本弄清楚了ThreadLocal相關設計和內存模型,對于是否會發生內存泄露做了分析,下面總結下幾點建議:

  • 當需要存儲線程私有變量的時候,可以考慮使用ThreadLocal來實現
  • 當需要實現線程安全的變量時,可以考慮使用ThreadLocal來實現
  • 當需要減少線程資源競爭的時候,可以考慮使用ThreadLocal來實現
  • 注意Thread實例和ThreadLocal實例的生存周期,因為他們直接關聯著存儲數據的生命周期
    • 如果頻繁的在線程中new ThreadLocal對象,在使用結束時,最好調用ThreadLocal.remove來釋放其value的引用,避免在ThreadLocal被回收時value無法被訪問卻又占用著內存

其實對于ThreadLocalMap還有很多設計,關于其詳細內容,可以參考文后參考文章的最后一篇

參考文章

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容