解讀ThreadLocal-盡量一篇包含所有疑問

java-ThreadLocal

ThreadLocal的實例代表了一個線程局部的變量涝焙,每條線程都只能看到自己的值锡凝,并不會意識到其它的線程中也存在該變量。它采用采用空間來換取時間的方式,解決多線程中相同變量的訪問沖突問題弹沽。

考慮一個問題:線程安全問題的核心在于多個線程會對同一個臨界區(qū)共享資源進行操作

  • synchronzed或者lock控制線程對臨界區(qū)資源的同步順序從而解決線程安全的問題。

  • ThreadLocal:表示線程的“本地變量”筋粗,即每個線程都擁有該變量副本策橘,每個線程都使用自己的“共享資源”,這就是一種“空間換時間”的方案娜亿。

ThreadLocal的內(nèi)部結(jié)構(gòu)

下圖是ThreadLocal的內(nèi)部結(jié)構(gòu)役纹,同時也簡單的給出來Thread對它的引用。

ThreadLocal.png

通過圖以及源碼 ThreadLocal內(nèi)部是基于一個ThreadLocalMap來實現(xiàn)暇唾,而ThreadLocalMap內(nèi)部又是一個Entry的數(shù)據(jù)結(jié)構(gòu)促脉。Entry的數(shù)據(jù)結(jié)構(gòu)是基于弱引用來使用辰斋。源碼結(jié)構(gòu)如下

public class ThreadLocal<T> {
   
    static class ThreadLocalMap {
      /**
       * The table, resized as necessary.
       * table.length MUST always be a power of two.
       */
      private Entry[] table;
      ...
       static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;
          Entry(ThreadLocal<?> k, Object v) {
                super(k);
          value = v;
        }
    }
    ...
}

ThreadLocal的數(shù)據(jù)結(jié)構(gòu)分析

ThreadLocalMap詳解

ThreadLocal內(nèi)部定義了ThreadLocalMap靜態(tài)內(nèi)部類,數(shù)據(jù)真正存放在ThreadLocalMap當(dāng)中瘸味,所以threadLocal的get宫仗,set和remove方法實際上具體是通過threadLocalMap的getEntry,set和remove方法實現(xiàn)的。

Entry詳解

ThreadLocalMap是threadLocal一個靜態(tài)內(nèi)部類旁仿,和大多數(shù)容器一樣內(nèi)部維護了一個數(shù)組藕夫,同樣的threadLocalMap內(nèi)部維護了一個Entry類型的table數(shù)組。
Entry是一個以ThreadLocal為key,Object為value的鍵值對枯冈,另外需要注意的是這里的threadLocal是弱引用毅贮,因為Entry繼承了WeakReference,在Entry的構(gòu)造方法中尘奏,調(diào)用了super(k)方法就會將threadLocal實例包裝成一個WeakReferenece滩褥。

threadLocal內(nèi)存泄漏問題

每個線程實例中可以通過threadLocals獲取到threadLocalMap,而threadLocalMap實際上就是一個以threadLocal實例為key炫加,任意對象為value的Entry數(shù)組瑰煎。當(dāng)我們?yōu)閠hreadLocal變量賦值,實際上就是以當(dāng)前threadLocal實例為key俗孝,值為value的Entry往這個threadLocalMap中存放酒甸。需要注意的是Entry中的key是弱引用,當(dāng)threadLocal外部強引用被置為null(threadLocalInstance=null),那么系統(tǒng) GC 的時候赋铝,根據(jù)可達性分析插勤,這個threadLocal實例就沒有任何一條鏈路能夠引用到它,這個ThreadLocal勢必會被回收革骨,這樣一來饮六,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value苛蒲,如果當(dāng)前線程再遲遲不結(jié)束的話卤橄,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收,造成內(nèi)存泄漏臂外。當(dāng)然窟扑,如果當(dāng)前thread運行結(jié)束,threadLocal漏健,threadLocalMap,Entry沒有引用鏈可達嚎货,在垃圾回收的時候都會被系統(tǒng)進行回收。在實際開發(fā)中蔫浆,會使用線程池去維護線程的創(chuàng)建和復(fù)用殖属,比如固定大小的線程池,線程為了復(fù)用是不會主動結(jié)束的瓦盛,所以洗显,threadLocal的內(nèi)存泄漏問題

ThreadLocal的數(shù)據(jù)操作分析

ThreadLocal的數(shù)據(jù)結(jié)構(gòu)與內(nèi)部組件外潜。通過提供的核心api了解這些內(nèi)部組件是如何進行數(shù)據(jù)存儲。

T get()

get方法是獲取當(dāng)前線程中threadLocal變量的值,具體步驟如下

  • 獲取當(dāng)前線程的實例對象
  • 獲取當(dāng)前線程的threadLocalMap
  • if -> map != null 獲取map中當(dāng)前threadLocal實例為key的值的entry
  • 當(dāng)前entitiy不為null的話挠唆,就返回相應(yīng)的值value
  • 若map為null或者entry為null的話通過setInitialValue方法初始化处窥,并返回該方法返回的value
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

threadLocal 實線程隔離,其實就是Thread內(nèi)部自己維護ThreadLocalMap的一個成員變量玄组,getMap(t);方法也是證明的這個理論滔驾。

public class Thread implements Runnable { 
  ...   
  /* ThreadLocal values pertaining to this thread. This map is maintained
   * by the ThreadLocal class. */
  ThreadLocal.ThreadLocalMap threadLocals = null;
  ...
}

setInitialValue 看下setInitialValue主要做了些什么事情?
邏輯和set方法幾乎一致俄讹,另外值得關(guān)注的是initialValue方法:protected修飾的也就是說繼承ThreadLocal的子類可重寫該方法哆致,實現(xiàn)賦值為其他的初始值


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;
}

protected T initialValue() {
    return null; 
}

get()總結(jié):

  • 通過當(dāng)前線程thread實例獲取到它所維護的threadLocalMap,
  • 然后以當(dāng)前threadLocal實例為key獲取該map中的鍵值對(Entry)患膛,若Entry不為null則返回Entry的value摊阀。
  • 如果獲取threadLocalMap為null或者Entry為null的話,就以當(dāng)前threadLocal為Key剩瓶,value為null存入map后驹溃,并返回null城丧。

void set(T value)

set方法設(shè)置在當(dāng)前線程中threadLocal變量的值.方法的邏輯很簡單延曙,數(shù)據(jù)value是真正的存放在了ThreadLocalMap這個容器中了,并且是以當(dāng)前threadLocal實例為key

    1. 獲取當(dāng)前線程實例對象亡哄;
    1. 通過當(dāng)前線程實例獲取到ThreadLocalMap對象枝缔;
    1. 如果Map不為null,則以當(dāng)前threadLocl實例為key,值為value進行存入;
  • 4.map為null,則新建ThreadLocalMap并存入value
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
       map.set(this, value);
    else  
       createMap(t, value); 
}

分析

線程使用 ThreadLocalMap 來存儲每個線程副本變量蚊惯,它是 ThreadLocal 里的一個靜態(tài)內(nèi)部類愿卸。ThreadLocalMap 也是采用的散列表(Hash)思想來實現(xiàn)的,但是實現(xiàn)方式和 HashMap 不太一樣截型。
補充:散列表1

  • 理想狀態(tài)下趴荸,散列表就是一個包含關(guān)鍵字的固定大小的數(shù)組,通過使用散列函數(shù)宦焦,將關(guān)鍵字映射到數(shù)組的不同位置发钝。

  • 在理想狀態(tài)下,哈希函數(shù)可以將關(guān)鍵字均勻的分散到數(shù)組的不同位置波闹,不會出現(xiàn)兩個關(guān)鍵字散列值相同(假設(shè)關(guān)鍵字?jǐn)?shù)量小于數(shù)組的大性秃馈)的情況。但是在實際使用中精堕,經(jīng)常會出現(xiàn)多個關(guān)鍵字散列值相同的情況(被映射到數(shù)組的同一個位置)孵淘,我們將這種情況稱為散列沖突。為了解決散列沖突歹篓,主要采用下面兩種方式:

  • 分離鏈表法(separate chaining)

  • 開放定址法(open addressing)

補充:分離鏈表法

  • 分散鏈表法使用鏈表解決沖突瘫证,將散列值相同的元素都保存到一個鏈表中揉阎。當(dāng)查詢的時候,首先找到元素所在的鏈表痛悯,然后遍歷鏈表查找對應(yīng)的元素余黎。
  • index key = hash_key % table.size;

補充:開放定址法

  • 開放定址法不會創(chuàng)建鏈表,當(dāng)關(guān)鍵字散列到的數(shù)組單元已經(jīng)被另外一個關(guān)鍵字占用的時候载萌,就會嘗試在數(shù)組中尋找其他的單元惧财,直到找到一個空的單元。探測數(shù)組空單元的方式有很多扭仁,這里介紹一種最簡單的 -- 線性探測法讥此。線性探測法就是從沖突的數(shù)組單元開始象踊,依次往后搜索空單元,如果到數(shù)組尾部,再從頭開始搜索(環(huán)形查找);
  • 如下圖:
開放定址法.jpg

ThreadLocalMap 中使用開放地址法來處理散列沖突藤树,而 HashMap 中使用的分離鏈表法。之所以采用不同的方式主要是因為:在 ThreadLocalMap 中的散列值分散的十分均勻瘦赫,很少會出現(xiàn)沖突箕般。并且 ThreadLocalMap 經(jīng)常需要清除無用的對象,使用純數(shù)組更加方便顽分。

看一下set方法徐许。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;
    //根據(jù)threadLocal的hashCode確定Entry應(yīng)該存放的位置
    int i = key.threadLocalHashCode & (len-1);

    //采用開放地址法,hash沖突的時候使用線性探測
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //覆蓋舊Entry
        if (k == key) {
            e.value = value;
            return;
        }
        //當(dāng)key為null時卒蘸,說明threadLocal強引用已經(jīng)被釋放掉雌隅,那么就無法
        //再通過這個key獲取threadLocalMap中對應(yīng)的entry,這里就存在內(nèi)存泄漏的可能性
        if (k == null) {
            //用當(dāng)前插入的值替換掉這個key為null的“臟”entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //新建entry并插入table中i處
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //插入后再次清除一些key為null的“臟”entry,如果大于閾值就需要擴容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

threadLocal的hashcode?
從源碼中我們可以清楚的看到threadLocal實例的hashCode是通過nextHashCode()方法實現(xiàn)的缸沃,該方法實際上總是用一個AtomicInteger加上0x61c88647來實現(xiàn)的恰起。0x61c88647這個數(shù)是有特殊意義的,它能夠保證hash表的每個散列桶能夠均勻的分布趾牧,這是Fibonacci Hashing检盼,也正是能夠均勻分布,所以threadLocal選擇使用開放地址法來解決hash沖突的問題翘单。

怎樣確定新值插入到哈希表中的位置吨枉?
key.threadLocalHashCode & (len-1),同hashMap和ConcurrentHashMap等容器的方式一樣县恕,利用當(dāng)前key(即threadLocal實例)的hashcode與哈希表大小相與东羹,因為哈希表大小總是為2的冪次方,所以相與等同于一個取模的過程忠烛,這樣就可以通過Key分配到具體的哈希桶中去属提。而至于為什么取模要通過位與運算的原因就是位運算的執(zhí)行效率遠(yuǎn)遠(yuǎn)高于了取模運算。

怎樣解決hash沖突?
源碼中通過nextIndex(i, len)方法解決hash沖突的問題冤议,該方法為((i + 1 < len) ? i + 1 : 0);斟薇,也就是不斷往后線性探測,當(dāng)?shù)焦1砟┪驳臅r候再從0開始恕酸,成環(huán)形堪滨。

怎樣解決“臟”Entry?
在set方法的for循環(huán)中尋找和當(dāng)前Key相同的可覆蓋entry的過程中通過replaceStaleEntry方法解決臟entry的問題蕊温。如果當(dāng)前table[i]為null的話袱箱,直接插入新entry后也會通過cleanSomeSlots來解決臟entry的問題。

如何進行擴容义矛?

private int threshold; // Default to 0
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;
    
    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);
    }
    
    /**
     * Set the resize threshold to maintain at worst a 2/3 load factor.
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
  • 第一次為threadLocal進行賦值的時候會創(chuàng)建初始大小為16的threadLocalMap,并且通過setThreshold方法設(shè)置threshold发笔,其值為當(dāng)前哈希數(shù)組長度乘以(2/3),也就是說加載因子為2/3(加載因子是衡量哈希表密集程度的一個參數(shù)凉翻,如果加載因子越大的話了讨,說明哈希表被裝載的越多,出現(xiàn)hash沖突的可能性越大制轰,反之前计,則被裝載的越少,出現(xiàn)hash沖突的可能性越小垃杖。同時如果過小男杈,很顯然內(nèi)存使用率不高,該值取值應(yīng)該考慮到內(nèi)存使用率和hash沖突概率的一個平衡缩滨,如hashMap,concurrentHashMap的加載因子都為0.75)势就。這里threadLocalMap初始大小為16泉瞻,加載因子為2/3脉漏,所以哈希表可用大小為:16*2/3=10,即哈希表可用容量為10袖牙。

  • 從set方法中可以看出當(dāng)hash表的size大于threshold的時候侧巨,會通過resize方法進行擴容

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //新數(shù)組為原數(shù)組的2倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            //遍歷過程中如果遇到臟entry的話直接另value為null,有助于value能夠被回收
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                //重新確定entry在新數(shù)組的位置,然后進行插入
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    //設(shè)置新哈希表的threshHold和size屬性
    setThreshold(newLen);
    size = count;
    table = newTab;
}

新建一個大小為原來數(shù)組長度的兩倍的數(shù)組鞭达,然后遍歷舊數(shù)組中的entry并將其插入到新的hash數(shù)組中司忱,主要注意的是,在擴容的過程中針對臟entry的話會令value為null畴蹭,以便能夠被垃圾回收器能夠回收坦仍,解決隱藏的內(nèi)存泄漏的問題

set()總結(jié):

  • 通過當(dāng)前線程對象thread獲取該thread所維護的threadLocalMap;
  • 若threadLocalMap不為null,則以threadLocal實例為key,值為value的鍵值對存入threadLocalMap;
  • 若threadLocalMap為null的話叨襟,就新建threadLocalMap然后在以threadLocal為鍵繁扎,值為value的鍵值對存入即可。

remove

從map中刪除數(shù)據(jù),先獲取與當(dāng)前線程相關(guān)聯(lián)的threadLocalMap然后從map中刪除該threadLocal實例為key的鍵值對即可梳玫。

    1. 獲取當(dāng)前線程的threadLocalMap;
    1. 從map中刪除以當(dāng)前threadLocal實例為key的鍵值對.
public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
   if (m != null)
       m.remove(this);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爹梁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子提澎,更是在濱河造成了極大的恐慌姚垃,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盼忌,死亡現(xiàn)場離奇詭異积糯,居然都是意外死亡,警方通過查閱死者的電腦和手機谦纱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門絮宁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人服协,你說我怎么就攤上這事绍昂。” “怎么了偿荷?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵窘游,是天一觀的道長。 經(jīng)常有香客問我跳纳,道長忍饰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任寺庄,我火速辦了婚禮艾蓝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斗塘。我一直安慰自己赢织,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布馍盟。 她就那樣靜靜地躺著于置,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贞岭。 梳的紋絲不亂的頭發(fā)上八毯,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機與錄音瞄桨,去河邊找鬼话速。 笑死,一個胖子當(dāng)著我的面吹牛芯侥,可吹牛的內(nèi)容都是我干的泊交。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼活合!你這毒婦竟也來了雏婶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤白指,失蹤者是張志新(化名)和其女友劉穎留晚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體告嘲,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡错维,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了橄唬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赋焕。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖仰楚,靈堂內(nèi)的尸體忽然破棺而出隆判,到底是詐尸還是另有隱情,我是刑警寧澤僧界,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布侨嘀,位于F島的核電站,受9級特大地震影響捂襟,放射性物質(zhì)發(fā)生泄漏咬腕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一葬荷、第九天 我趴在偏房一處隱蔽的房頂上張望涨共。 院中可真熱鬧,春花似錦宠漩、人聲如沸举反。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽照筑。三九已至吹截,卻和暖如春瘦陈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背波俄。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工晨逝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懦铺。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓捉貌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子趁窃,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355