ThreadLocal--以副本的方式解決并發(fā)以及隔離問題

不論是Atomic還是synchronized或者Lock耍贾,其實都是采用同步的方式(串行或者自旋等)解決了線程安全問題墩弯。這里我們將介紹另外一種解決線程安全問題的思路----副本的方式昔榴。

如果你有一個全局共享的變量牲尺,那么多線程并發(fā)的時候,對這個共享變量的訪問是不安全的。方法內(nèi)的局部變量是線程安全的缆瓣,由于每個線程都會有自己的副本虹统。也就是說局部變量被封閉在線程內(nèi)部弓坞,其它線程無法訪問(引用型有所區(qū)別)。那么有沒有作用域介于兩者之間车荔,既能保證線程安全渡冻,又不至于只局限于方法內(nèi)部的方式呢?答案是肯定的忧便,我們使用ThreadLocal就可以做到這一點族吻。ThreadLocal變量的作用域是為線程,也就是說線程內(nèi)跨方法共享珠增。例如某個對象的方法A對threadLocal變量賦值超歌,在同一個線程中的另外一個對象的方法B能夠讀取到該值。因為作用域為同一個線程蒂教,那么自然就是線程安全的巍举。但是需要注意的是,如果threadLocal存儲的是共享變量的引用凝垛,那么同樣會有線程安全問題懊悯。

1蜓谋、ThreadLocal 的使用場景

ThreadLocal的特性決定了它的使用場景。由于ThreadLocal中存儲的變量是線程隔離的炭分,所以一般在以下情況使用ThreadLocal:

1桃焕、存儲需要在線程隔離的數(shù)據(jù)。比如線程執(zhí)行的上下文信息捧毛,每個線程是不同的覆旭,但是對于同一個線程來說會共享同一份數(shù)據(jù)。Spring MVC的 RequestContextHolder 的實現(xiàn)就是使用了ThreadLocal岖妄;

2型将、跨層傳遞參數(shù)。層次劃分在軟件設計中十分常見荐虐。層次劃分后七兜,體現(xiàn)在代碼層面就是每層負責不同職責,一個完整的業(yè)務操作福扬,會由一系列不同層的類的方法調(diào)用串起來完成腕铸。有些時候第一層獲得的一個變量值可能在第三層、甚至更深層的方法中才會被使用铛碑。如果我們不借助ThreadLocal狠裹,就只能一層層地通過方法參數(shù)進行傳遞。使用ThreadLocal后汽烦,在第一層把變量值保存到ThreadLocal中涛菠,在使用的層次方法中直接從ThreadLocal中取出,而不用作為參數(shù)在不同方法中傳來傳去撇吞。不過千萬不要濫用ThreadLocal俗冻,它的本意并不是用來跨方法共享變量的。結(jié)合第一種情況牍颈,我們放入ThreadLocal跨層傳遞的變量一般也是具有上下文屬性的迄薄。比如用戶的信息等。這樣我們在AOP處理異持笏辏或者其他操作時可以很方便地獲取當前登錄用戶的信息讥蔽。

2、如何使用 ThreadLocal

ThreadLocal使用起來非常簡單画机,我們先看一個簡單的例子冶伞。

可以看到每個線程為同一個ThreadLocal對象set不同的值,但各個線程打印出來的依舊是自己保存進去的值色罚,并沒有被其它線程所覆蓋碰缔。

一般來說,在實踐中戳护,我們會把ThreadLocal對象聲名為static final金抡,作為私有變量封裝到自定義的類中瀑焦。另外提供static的set和get方法。如下面的代碼:

public final class OperationInfoRecorder {

    private static final ThreadLocal<OperationInfoDTO> THREAD_LOCAL = new ThreadLocal<>();

    private OperationInfoRecorder() {
    }

    public static OperationInfoDTO get() {
        return THREAD_LOCAL.get();
    }

    public static void set(OperationInfoDTO operationInfoDTO) {
        THREAD_LOCAL.set(operationInfoDTO);
    }

    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

這樣做的目的有二:

  • 1梗肝、static 確保全局只有一個保存OperationInfoDTO對象的ThreadLocal實例榛瓮;
  • 2、final 確保ThreadLocal的實例不可更改巫击。防止被意外改變禀晓,導致放入的值和取出來的不一致。另外還能防止ThreadLocal的內(nèi)存泄漏坝锰,具體原因下文中會有講解粹懒。

使用的時候可以在任何方法的任何位置調(diào)用OperationInfoRecorder的set或者get方法,保存和取出顷级。如下面代碼:

OperationInfoRecorder.set(operationInfoDTO)
OperationInfoRecorder.get()

3凫乖、ThreadLocal源代碼解析

學習到這里,你一定很好奇ThreadLocal是如何做到多個線程對同一個對象set操作弓颈,但只會get出自己set進去的值呢帽芽?這個現(xiàn)象有點違背我們的認知。接下來我們就從set方法入手翔冀,來看看ThreadLocal的源代碼:

   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。沒錯纤子,如果ThreadLocal能夠保存多個線程的變量值搬瑰,那么它一定是借助容器來實現(xiàn)的。

這個map不是一般的map计福,可以看到它是通過當前線程對象獲取到的ThreadLocalMap跌捆』罩埃看到這里應該看出些端倪象颖,這個map其實是和Thread綁定的。接下來我們看getMap方法的代碼:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

原來這個ThreadLocal就存方法Thread對象上姆钉。下面我們看看Thread中的相關(guān)代碼:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

注釋中寫的很清楚说订,這個屬性由ThreadLocal來維護。threadLocals的訪問控制決定在包外是無法直接訪問的潮瓶。所以我們在使用的時候只能通過ThreadLocal對象來訪問陶冷。

set時,會把當前threadLocal對象作為key毯辅,你想要保存的對象作為value埂伦,存入map。

看到這里思恐,我們大至已經(jīng)理清了ThreadLocal和Thread的關(guān)系沾谜,我們看下圖:

我們接下來分析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();
}

get方法也是先取得當前線程對象中保存的ThreadLocalMap對象,然后使用當前threadLocal對象從map中取得相應的value基跑。

每個Thread的ThreadMap以threadLocal作為key婚温,保存自己線程的value副本。我們可以這么來理解ThreadLocal媳否,其實ThreadLocal對象是你要真正保存對象的身份代表栅螟。而這個身份在每個線程中對應的值,其實是保存在每個線程中篱竭,并沒有保存在ThreadLocal對象中力图。

這里可以舉個例子,學校里要每班評選一名學習標兵掺逼,一名道德標兵搪哪。班主任會進行評選然后記錄下來。學生標兵及道德標兵的身份就是兩個ThreadLocal對象坪圾,而每個班主任是一個線程晓折,記錄的評選結(jié)果的小本子就是ThreadLocalMap對象。每個班主任會在自己的小本子上記錄下評選結(jié)果兽泄,比如說一班班主任記錄:道德標兵:小明漓概,學習標兵:小紅。二班班主任記錄:道德標兵:小趙病梢,學習標兵:小巖胃珍。通過這個例子大家應該很清楚ThreadLocal的原理了。

ThreadLocal的設計真的非常巧妙蜓陌,看似自己保存了每個線程的變量副本觅彰,其實每個線程的變量副本是保存在線程對象中,那么自然就線程隔離了钮热。如此分析起來填抬,是不是有一種ThreadLocal沒做什么事情,卻搶了頭功的感覺隧期?其實不然飒责。Thread對象中用來保存變量副本的ThreadLocalMap的定義就在ThreadLocal中。我們接下來分析ThreadLocalMap的源代碼仆潮。

4宏蛉、ThreadLocalMap分析

ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類。ThreadLocalMap的功能其實是和HashMap類似的性置,但是為什么不直接使用HashMap呢拾并?在ThreadLocalMap中使用WeakReference包裝后的ThreadLocal對象作為key,也就是說這里對ThreadLocal對象為弱引用。當ThreadLocal對象在ThreadLocalMap引用之外嗅义,再無其他引用的時候能夠被垃圾回收个榕。如下面代碼所示:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

這樣做會帶來新的問題。如果ThreadLocal對象被回收芥喇,那么ThreadLocalMap中保存的key值就變成了null西采,而value會一直被Entry引用,而Entry又被threadLocalMap對象引用继控,threadLocalMap對象又被Thread對象所引用械馆,那么當Thread一直不終結(jié)的話,value對象就會一直駐留在內(nèi)存中武通,直至Thread被銷毀后霹崎,才會被回收。這就是ThreadLocal引起內(nèi)存泄漏問題冶忱。

如上圖所示尾菇,由于ThreadLocal對象是弱引用,如果外部沒有強引用指向它囚枪,它就會被GC回收派诬,導致Entry的Key為null 由于ThreadLocal對象是弱引用,如果外部沒有強引用指向它链沼,它就會被GC回收默赂,導致Entry的Key為null 如果當前的情況下在棧中將threadlocal1的引用設置為null,強引用1將會失效括勺,那堆中的threadlocal1對象因為ThreadLocalMap的key對它的引用是弱引用缆八,將會在下一次gc被回收,那就會出現(xiàn)key變成null疾捍,如果這時value外部也沒有強引用指向它奈辰,那么value就永遠也訪問不到了,按理也應該被GC回收乱豆,但是由于ThreadLocalMap.Entry對象還在強引用value奖恰,導致value無法被回收,這時「內(nèi)存泄漏」就發(fā)生了咙鞍,value成了一個永遠也無法被訪問房官,但是又無法被回收的對象。

而ThreadLocalMap在設計的時候也考慮到這一點续滋,在get和set的時候,會把遇到的key為null的entry清理掉孵奶。不過這樣做是依賴于我們后面對ThreadLocal的持續(xù)使用也不能100%保證能夠清理干凈疲酌,如果我們在秒殺服務中使用,有可能造成內(nèi)存的瞬間打滿。

通常朗恳,我們可以通過以下兩種方式來避免這個問題:

  • 1湿颅、把ThreadLocal對象聲明為static,這樣ThreadLocal成為了類變量粥诫,生命周期不是和對象綁定油航,而是和類綁定,延長了聲明周期怀浆,避免了被回收谊囚;
  • 2、在使用完ThreadLocal變量后执赡,手動remove掉镰踏,防止ThreadLocalMap中Entry一直保持對value的強引用。導致value不能被回收沙合。
  • 3奠伪、減少損害,盡量不要在ThreadLocal中放大對象

4首懈、總結(jié)

通過本節(jié)學習绊率,我們掌握了ThreadLocal 的原理和其使用場景。絕大多數(shù)情況下究履,ThreadLocal用于存儲和線程相關(guān)的上下文信息即舌,也就是線程共享的信息,便于同一線程的不同方法中取值挎袜,而不用作為方法參數(shù)層層傳遞顽聂。

使用的時候需要注意幾個常見的問題1.內(nèi)存泄漏 2.上下文丟失(常見于線程池,并行流)3.數(shù)據(jù)交互污染(常見于線程池)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末盯仪,一起剝皮案震驚了整個濱河市紊搪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌全景,老刑警劉巖耀石,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異爸黄,居然都是意外死亡滞伟,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進店門炕贵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梆奈,“玉大人,你說我怎么就攤上這事称开∧吨樱” “怎么了乓梨?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長清酥。 經(jīng)常有香客問我扶镀,道長,這世上最難降的妖魔是什么焰轻? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任臭觉,我火速辦了婚禮,結(jié)果婚禮上辱志,老公的妹妹穿的比我還像新娘蝠筑。我一直安慰自己,他們只是感情好荸频,可當我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布菱肖。 她就那樣靜靜地躺著,像睡著了一般旭从。 火紅的嫁衣襯著肌膚如雪稳强。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天和悦,我揣著相機與錄音退疫,去河邊找鬼。 笑死鸽素,一個胖子當著我的面吹牛褒繁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播馍忽,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼棒坏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遭笋?” 一聲冷哼從身側(cè)響起呈枉,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤饱苟,失蹤者是張志新(化名)和其女友劉穎坑夯,沒想到半個月后庐椒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡央串,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年磨澡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片质和。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡稳摄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侦另,到底是詐尸還是另有隱情秩命,我是刑警寧澤尉共,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布褒傅,位于F島的核電站弃锐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏殿托。R本人自食惡果不足惜霹菊,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望支竹。 院中可真熱鬧旋廷,春花似錦、人聲如沸礼搁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馒吴。三九已至扎运,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饮戳,已是汗流浹背豪治。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扯罐,地道東北人负拟。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像歹河,于是被迫代替她去往敵國和親掩浙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,554評論 2 349

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