ThreadLocal小記


title: ThreadLocal小記
date: 2016-09-11 19:33:39
tags: Java
categories: Java


ThreadLocal

最近《Java并發(fā)編程實戰(zhàn)》第三章談及了對象共享的問題艇潭。對共享的可變數(shù)據(jù)最簡單粗暴的做法當然是同步,但是同步的缺點也很明顯:1. 同步會導致阻塞;2.代碼復雜可維護性降低蹋凝。針對這個問題鲁纠,書上談及到了通過線程封閉避免同步,其中的ThreadLocal類就是幫助維持線程封閉性的仙粱。

之前對ThreadLocal的認識非常簡單房交,就是把一個變量綁定到線程上彻舰。參照網(wǎng)上的例子自己也實現(xiàn)了類似功能的例子ThreadLocalVariable(https://github.com/zhanghTK/HelloJava/blob/master/src/main/java/tk/zhangh/java/thread/ThreadLocalVariable.java)伐割,但總的來說沒什么體會。今天參照別人的文章看了一下ThreadLocal的實現(xiàn)刃唤,發(fā)現(xiàn)還是有蠻多注意點的隔心。

看JDK之前想當然的以為ThreadLocal應該就是簡單對Map<Thread, Object>做一個封裝,然而實際并沒有這么簡單尚胞。參照了網(wǎng)上一些文章的說法硬霍,早期的ThreadLocal確實是這樣實現(xiàn)的。但是這樣實現(xiàn)存在一些問題:

  1. 線程安全問題笼裳,如果使用線程安全的Map實現(xiàn)那么就會帶來性能問題唯卖,當有大量的線程使用ThreadLocal,伴隨著線程生命周期ThreadLocal也需要頻繁向底層的Map添加刪除數(shù)據(jù)躬柬。
  2. 內(nèi)存回收問題拜轨,用Thread當key,除非手動調(diào)用remove允青,否則即使線程退出了會導致:1)該Thread對象無法回收橄碾;2)該線程在所有ThreadLocal中對應的value也無法回收。

ThreadLocal實際給出了不同的實現(xiàn)方式颠锉。首先綁定到線程的變量沒有維護在ThreadLocal內(nèi)法牲,而是維護在各個Thread類實例內(nèi)——在Thread類內(nèi)使用了ThreadLocal的靜態(tài)內(nèi)部類ThreadLocalMap實例去維護需要綁定到線程的變量。這樣原本需要維護在ThreadLocal內(nèi)的數(shù)據(jù)現(xiàn)在就分散到了各個線程內(nèi)去維護琼掠。

在Thread中ThreadLocalMap的聲明長這樣:

// 真的就只是聲明了一下拒垃,什么都沒干    
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal一共只有五個非私有的方法,首先是兩個并沒有什么卵用的方法:

// 構造瓷蛙,什么都沒干
public ThreadLocal() {}

// 設置ThreadLocal的初始值悼瓮,protected很明顯是希望子類重寫
protected T initialValue() {return null}

看看其余三個方法的實現(xiàn)(JDK8)

    public T get() {
        Thread t = Thread.currentThread();
        // 從線程里獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 根據(jù)ThreadLocal實例獲取Entity
            // 注意key類型是ThreadLocal,不是Thread
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果獲得的map為null或者從map通過key獲取的value為空時獲取一個初始值
        // setInitialValue方法里調(diào)用了initialValue方法初始化一個值
        return setInitialValue();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 沒什么說的set進去
            map.set(this, value);
        else
            // 當map不存在時速挑,使用初始值創(chuàng)建一個
            createMap(t, value);
    }

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             // 沒什么說的谤牡,remove掉
             m.remove(this);
     }
    
    ThreadLocalMap getMap(Thread t) {
        // 你給我一個線程,還你一個map
        return t.threadLocals;
    }

ThreadLocal所有的操作都是圍繞著內(nèi)部類ThreadLocalMap的姥宝,ThreadLocal只是讓ThreadLocalMap更加容易訪問翅萤。咦,有點耳熟,沒錯外觀模式套么。

下面看一下ThreadLocal的靜態(tài)內(nèi)部類ThreadLocalMap培己,JDK文檔對它的描述主要集中在一下幾點:

  1. 是什么:定制的hash map用于維護本地線程變量
  2. 可見性:ThreadLocal之外沒有任何方法可訪問,Thread類使用它定義了私有屬性
  3. 特殊性:為了管理大對象胚泌、長生命周期對象省咨,ThreadLocalMap的key繼承WeakReference

前面兩點沒什么好說的,主要是第三點:key繼承了弱引用類型(關于弱引用可以先參考傳送門)玷室。

image

實線表示強引用零蓉,虛線表示弱引用。

簡而言之穷缤,一個對象在沒有強引用引用敌蜂,只有弱引用引用時,當GC發(fā)生這個對象就會被標記回收津肛。將ThreadLocalMap的key設置成弱引用章喉,
當不再使用ThreadLocal(沒有任何強引用引用)時,只有ThreadLocalMap的Entry對它存在弱引用身坐,這樣GC的時候這個ThreadLocal對象就可以被回收了秸脱。

key得到有效回收,但value依然存在著強引用關系部蛇√剑可能導致key被回收了,但value的引用始終存在搪花。引用關系:

Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value

即使線程不再使用 ThreadLocal遏片,但因為上面的引用關系存在仍會導致 value 無法回收,只有線程被銷毀時才能被回收撮竿。極端情況下(例如使用線程池管理線程)吮便,可能導致內(nèi)存溢出、數(shù)據(jù)錯亂幢踏。

真對這個問題ThreadLocalMap在實現(xiàn)的時候也采取了一些防護措施髓需,比如ThreadLocalMap的get(set,remove方法也類似房蝉,限于篇幅不展開了):

    private Entry getEntry(ThreadLocal<?> key) {
        // hash函數(shù)獲取索引位置
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            // 命中了
            return e;
        else
            // miss了
            return getEntryAfterMiss(key, i, e);
    }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        
        // 遍歷table
        while (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == key)
                // 找到了
                return e;
            if (k == null)
                // 發(fā)現(xiàn)key為空(也就是上面描述的內(nèi)存泄漏的情況)僚匆,做刪除
                expungeStaleEntry(i);
            else
                // 找下一個Entty位置
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

在get的過程中凡是碰到了key為null的Entry,這個Entry就會被擦除搭幻,從而避免內(nèi)存泄漏咧擂。類似的思路在set,remove方法中都有實現(xiàn)檀蹋。針對內(nèi)存泄漏問題ThreadLocal實際是需要手動觸發(fā)函數(shù)刪除key為null的Entry松申,因此對ThreadLocal維護的變量不再使用時,手動 remove 還是很有必要的。

ThreadLocal 還是有很多值得學習的地方贸桶,尤其是 ThreadLocalMap 設計的非常巧妙:

  • 從設計模式的角度:
    • 將線程內(nèi)使用的變量封裝在線程內(nèi)優(yōu)于全局的上線文(迪米特法則)
    • ThreadLocal 屏蔽了 ThreadLocalMap 的實現(xiàn)舅逸,為客戶端提供簡單統(tǒng)一的調(diào)用方法(外觀模式)
  • Java 實現(xiàn):
    • ThreadLocalMap 的 key 使用弱引用可以盡快回收 ThreadLocal,對 map 的其他操作也提供了 value 回收的兜底方式皇筛,最大限度的避免了內(nèi)存溢出的可能

ThreadLocal 的最佳實踐:在業(yè)務周期完成時琉历,顯式的調(diào)用ThreadLocal 的 remove()方法。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末水醋,一起剝皮案震驚了整個濱河市旗笔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌离例,老刑警劉巖换团,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宫蛆,居然都是意外死亡,警方通過查閱死者的電腦和手機的猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門耀盗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人卦尊,你說我怎么就攤上這事叛拷。” “怎么了岂却?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵忿薇,是天一觀的道長。 經(jīng)常有香客問我躏哩,道長署浩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任扫尺,我火速辦了婚禮筋栋,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好极谊,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布诗鸭。 她就那樣靜靜地躺著,像睡著了一般兄渺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天捣域,我揣著相機與錄音,去河邊找鬼。 笑死竟宋,一個胖子當著我的面吹牛提完,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丘侠,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼徒欣,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜗字?” 一聲冷哼從身側響起打肝,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挪捕,沒想到半個月后粗梭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡级零,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年断医,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奏纪。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鉴嗤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出序调,到底是詐尸還是另有隱情醉锅,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布发绢,位于F島的核電站硬耍,受9級特大地震影響,放射性物質發(fā)生泄漏边酒。R本人自食惡果不足惜经柴,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甚纲。 院中可真熱鬧口锭,春花似錦、人聲如沸介杆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽春哨。三九已至荆隘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赴背,已是汗流浹背椰拒。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工晶渠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人燃观。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓褒脯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缆毁。 傳聞我的和親對象是個殘疾皇子番川,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容