ThreadLocal原理解析與注意事項

一、引言

ThreadLocal是并發(fā)場景下用來解決變量共享問題的類瓦呼,它能使原本線程間共享的對象進行線程隔離涨共,即一個對象只對一個線程可見。但由于過度設計抡四,比如使用弱引用和哈希碰撞柜蜈,導致理解難度大、使用成本高指巡,反而成為故障高發(fā)點淑履,容易出現(xiàn)內(nèi)存泄漏、臟數(shù)據(jù)藻雪、共享對象更新等問題秘噪。

本文從Java引用類型、ThreadLocal源碼解析勉耀、ThreadLocal使用注意事項三個方面展開指煎。首先來看一段ThreadLocal的使用示例:

// ThreadLocal使用示例
public class ThreadLocalUtil {
    private static final ThreadLocal<Integer> testThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(4);
        for (int i = 0; i < 4; i ++) {
            new Thread(new TestThread(barrier)).start();
        }
    }

    static class TestThread implements Runnable{
        private CyclicBarrier barrier;

        public TestThread(CyclicBarrier barrier) {
            this.barrier = barrier;
        }
        
        @Override
        public void run() {
            try {
                barrier.await();
                for (int i = 0; i < 100; i++) {
                    Integer value = testThreadLocal.get();
                    if (value == null) {
                        value = 0;
                    }
                    Integer sum = value + i;
                    testThreadLocal.set(sum);
                }
                System.out.println(Thread.currentThread().getName() + " sum is " + testThreadLocal.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

從運行結(jié)果可以看出,每個線程的求和結(jié)果都是4950便斥,線程之間沒有相互影響至壤。

二、引用類型

前面有篇文章介紹過JVM垃圾回收的可達性分析機制枢纠。對象在堆上創(chuàng)建之后所持有的引用是一種變量類型像街,引用的可達性是判斷能否被垃圾回收的基本條件。我們可以把引用分為強引用晋渺、軟引用镰绎、弱引用和虛引用四類。

  • 強引用(Strong Reference):最為常見些举。如Object object = new Object();這樣的變量聲明和定義就會產(chǎn)生對該對象的強引用跟狱。只要對象有強引用指向,并且GC Roots可達户魏,那么Java內(nèi)存回收時驶臊,即使瀕臨內(nèi)存耗盡挪挤,也不會回收該對象。
  • 軟引用(Soft Reference):引用力度弱于強引用关翎,用于非必須對象上扛门。在即將OOM之前,垃圾回收器會把這些軟引用指向的對象加入回收范圍纵寝,以獲得更多的內(nèi)存空間论寨。軟引用主要用來緩存中間計算結(jié)果及不需要實時保存的用戶行為等。
  • 弱引用(Weak Reference):引用強度較前兩者更弱爽茴,也是用來描述非必需對象的葬凳。如果弱引用指向的對象只存在弱引用這條線路,則在下一次YGC時會被回收室奏。由于YGC時間的不確定性火焰,弱引用何時被回收也具有不確定性。調(diào)用 WeakReference. get()可能返回null胧沫,要注意空指針異常昌简。
  • 虛引用(Phantom Reference):是極弱的一種引用關(guān)系,定義完成后就無法通過該引用獲取指向的對象绒怨。為一個對象設置虛引用的唯一目的就是希望能在這個對象被回收時收到一個系統(tǒng)通知纯赎。虛引用必須與引用隊列聯(lián)合使用,當垃圾回收時南蹂,如果發(fā)現(xiàn)存在虛引用犬金,就會在回收對象內(nèi)存前,把這個虛引用加入與之關(guān)聯(lián)的引用隊列中六剥。

除強引用外佑附,其他三種引用可以減少對象在生命周期中所占用的內(nèi)存大小。使用這些引用仗考,需要注意強引用劫持音同、內(nèi)存泄漏等問題。

三秃嗜、ThreadLocal原理

自己實現(xiàn)ThreadLocal权均?

如果讓我們自己實現(xiàn)一個ThreadLocal,可能最直接的想法就是維護一個map锅锨,將線程作為key來獲取該線程的value叽赊,例如下面這段代碼:

public class MyThreadLocal<T> {
 
    private Map<Thread, T> keyValueMap = new WeakHashMap<>();
 
    public synchronized void set(T value) {
        Thread thread = Thread.currentThread();
        keyValueMap.put(thread, value);
    }
    
    public synchronized T get() {
        Thread thread = Thread.currentThread();
        return keyValueMap.get(thread);
    }
    
    public synchronized void remove() {
        Thread thread = Thread.currentThread();
        keyValueMap.remove(thread);
    }
}

這段代碼最大的問題就是synchronized的使用會導致并發(fā)性能較差。那么必搞,jdk中的ThreadLocal是如何實現(xiàn)的呢必指?

ThreadLocal類源碼分析

set方法

先來看set方法,源碼如下:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
 
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到恕洲,set方法是通過一個ThreadLocalMap對象來set值塔橡,而ThreadLocalMap雖然是ThreadLocal的靜態(tài)內(nèi)部類梅割,卻是Thread類中的成員變量。這跟我們設想的完全不一樣葛家!

get方法

再來看get方法户辞,源碼:

    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();
    }
 
    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方法也是將操作委托給了ThreadLocalMap,通過最終獲得ThreadLocalMap.Entry來獲取最終的值癞谒。

remove方法

最后來看remove方法底燎,同樣是交給ThreadLocalMap來處理

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap源碼分析

上文說過,ThreadLocalMap是線程的私有成員變量弹砚。我理解這樣做是為了避免多線程競爭双仍,因為放在Thread對象中就相當于線程私有了,處理的時候不需要加鎖桌吃。由于ThreadLocal本身的設計就是變量不與其他線程共享殊校,不需要其他線程訪問本對象的變量,放在Thread對象中不會有問題读存。

Entry數(shù)據(jù)結(jié)構(gòu)

ThreadLocalMap維護了一個Entry類型的數(shù)據(jù)結(jié)構(gòu):

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private Entry[] table;

Entry是一個以ThreadLocal為key,Object為value的鍵值對呕屎。需要注意的是让簿,threadLocal是弱引用,即使線程正在執(zhí)行中秀睛,只要ThreadLocal對象引用被置成null, Entry的Key就會自動在下一次YGC時被垃圾回收尔当。而在 ThreadLocal使用set()和get()時,又會自動地將那些 key==null 的value置為null蹂安,使value能夠被垃圾回收椭迎,避免內(nèi)存泄漏。

但是理想很豐滿田盈,現(xiàn)實很骨感畜号,ThreadLocal也因為這樣的設計導致了一些問題,下文會講到允瞧。

set方法

    private void set(ThreadLocal<?> key, Object value) { 
        Entry[] tab = table;
        int len = tab.length;
        // 定位Entry存放的位置
        int i = key.threadLocalHashCode & (len-1);
        // 處理hash沖突的情況简软,這里采用的是開放地址法
        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;
            }
        }
        // 新建entry并插入
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 清除臟數(shù)據(jù),擴容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

get方法

    private Entry getEntry(ThreadLocal<?> key) {
        // 確定entry位置
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 命中
        if (e != null && e.get() == key)
            return e;
        else
            // 存在hash沖突述暂,繼續(xù)查找
            return getEntryAfterMiss(key, i, e);
    }
 
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
 
        while (e != null) {
            ThreadLocal<?> k = e.get();
            //找到entry
            if (k == key)
                return e;
            // 臟數(shù)據(jù)處理
            if (k == null)
                expungeStaleEntry(i);
            else
                //遍歷
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

remove方法

   private void remove(ThreadLocal<?> key) {
        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)]) {
            if (e.get() == key) {
                //清空key
                e.clear();
                //清空value
                expungeStaleEntry(i);
                return;
            }
        }
    }

回頭梳理

我們再回頭梳理一下ThreadLocal和Thread的類關(guān)系圖與堆棧關(guān)系圖:

類關(guān)系圖
堆棧關(guān)系圖

上圖中的關(guān)系簡要概括:

  • 1個Thread有且僅有1個ThreadLocalMap對象
  • 1個Entry對象的key弱引用指向1個ThreadLocal對象
  • 1個ThreadLocalMap對象存儲多給Entry對象
  • 1個ThreadLocal對象可以被多個結(jié)程所共享
  • ThreadLocal對象不持有Value痹升,Value由線程的Entry對象持有

四、ThreadLocal注意事項

臟數(shù)據(jù)

線程復用會產(chǎn)生臟數(shù)據(jù)畦韭。由于結(jié)程池會重用Thread對象疼蛾,那么與Thread綁定的類的靜態(tài)屬性ThreadLocal變量也會被重用。如果在實現(xiàn)的線程run()方法體中不顯式地調(diào)用remove() 清理與線程相關(guān)的ThreadLocal信息艺配,那么倘若下一個結(jié)程不調(diào)用set() 設置初始值察郁,就可能get() 到重用的線程信息衍慎,包括 ThreadLocal所關(guān)聯(lián)的線程對象的value值。

內(nèi)存泄漏

通常我們會使用使用static關(guān)鍵字來修飾ThreadLocal(這也是在源碼注釋中所推薦的)绳锅。在此場景下西饵,其生命周期就不會隨著線程結(jié)束而結(jié)束,寄希望于ThreadLocal對象失去引用后鳞芙,觸發(fā)弱引用機制來回收Entry的Value就不現(xiàn)實了眷柔。如果不進行remove() 操作,那么這個線程執(zhí)行完成后原朝,通過ThreadLocal對象持有的對象是不會被釋放的驯嘱。

以上兩個問題的解決辦法很簡單,就是在每次用完ThreadLocal時喳坠, 必須要及時調(diào)用 remove()方法清理鞠评。

父子線程共享線程變量

很多場景下通過ThreadLocal來透傳全局上下文,會發(fā)現(xiàn)子線程的value和主線程不一致壕鹉。比如用ThreadLocal來存儲監(jiān)控系統(tǒng)的某個標記位剃幌,暫且命名為traceId。某次請求下所有的traceld都是一致的晾浴,以獲得可以統(tǒng)一解析的日志文件负乡。但在實際開發(fā)過程中,發(fā)現(xiàn)子線程里的traceld為null脊凰,跟主線程的并不一致抖棘。這就需要使用InheritableThreadLocal來解決父子線程之間共享線程變量的問題,使整個連接過程中的traceId一致狸涌。

參考資料

  • ThreadLocal源碼
  • 《碼出高效》
  • 慕珵:《ThreadLocal源碼學習》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末切省,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子帕胆,更是在濱河造成了極大的恐慌朝捆,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件懒豹,死亡現(xiàn)場離奇詭異右蹦,居然都是意外死亡,警方通過查閱死者的電腦和手機歼捐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門何陆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人豹储,你說我怎么就攤上這事贷盲。” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵巩剖,是天一觀的道長铝穷。 經(jīng)常有香客問我,道長佳魔,這世上最難降的妖魔是什么曙聂? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮鞠鲜,結(jié)果婚禮上宁脊,老公的妹妹穿的比我還像新娘。我一直安慰自己贤姆,他們只是感情好榆苞,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著霞捡,像睡著了一般坐漏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碧信,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天赊琳,我揣著相機與錄音,去河邊找鬼砰碴。 笑死躏筏,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的衣式。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼檐什,長吁一口氣:“原來是場噩夢啊……” “哼碴卧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起乃正,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤住册,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瓮具,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荧飞,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年名党,在試婚紗的時候發(fā)現(xiàn)自己被綠了叹阔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡传睹,死狀恐怖耳幢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤睛藻,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布启上,位于F島的核電站,受9級特大地震影響店印,放射性物質(zhì)發(fā)生泄漏冈在。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一按摘、第九天 我趴在偏房一處隱蔽的房頂上張望包券。 院中可真熱鬧,春花似錦院峡、人聲如沸兴使。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽发魄。三九已至,卻和暖如春俩垃,著一層夾襖步出監(jiān)牢的瞬間励幼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工口柳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留苹粟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓跃闹,卻偏偏與公主長得像嵌削,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子望艺,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

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