不論是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)存泄漏問題冶忱。
而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ù)交互污染(常見于線程池)