Java多線程:ThreadLocal
一、ThreadLocal基礎知識
ThreadLocal是(shi)線程的一個(ge)本地化對(dui)象,或者說是(shi)局(ju)部變量。當工(gong)作于多(duo)線程(cheng)(cheng)中的(de)對象使用ThreadLocal維(wei)護變(bian)量時(shi),ThreadLocal為每(mei)個(ge)使用該(gai)變(bian)量的(de)線程(cheng)(cheng)分配一(yi)個(ge)獨(du)立(li)的(de)變(bian)量副本。所以(yi)每(mei)一(yi)個(ge)線程(cheng)(cheng)都可以(yi)獨(du)立(li)地改變(bian)自己的(de)副本,而不會影(ying)響其他線程(cheng)(cheng)所對應的(de)副本。
ThreadLocal不是用來解決對象(xiang)共享訪問問題的(de),而主要是提供了線程(cheng)保持對象(xiang)的(de)方(fang)法和(he)避免(mian)參數傳遞的(de)方(fang)便(bian)的(de)對象(xiang)訪問方(fang)式(shi)
ThreadLocal的應(ying)用(yong)場合,最適合的是按線程(cheng)多實(shi)例(每個(ge)(ge)線程(cheng)對應(ying)一個(ge)(ge)實(shi)例)的對象的訪(fang)問,并且這(zhe)個(ge)(ge)對象很多地(di)方(fang)都要(yao)用(yong)到。
概括起來(lai)說,對于(yu)多線(xian)程資源共享的問題,同(tong)(tong)步機制(zhi)synchronized采(cai)(cai)用(yong)了“以時(shi)(shi)間換空間”的方(fang)式,比(bi)如(ru)定義一(yi)個(ge)static變(bian)量,同(tong)(tong)步訪(fang)問,而ThreadLocal采(cai)(cai)用(yong)了“以空間換時(shi)(shi)間”的方(fang)式。前者僅提(ti)供(gong)(gong)一(yi)份(fen)變(bian)量,讓不(bu)同(tong)(tong)的線(xian)程排隊訪(fang)問,而后(hou)者為(wei)每一(yi)個(ge)線(xian)程都提(ti)供(gong)(gong)了一(yi)份(fen)變(bian)量,因(yin)此可以同(tong)(tong)時(shi)(shi)訪(fang)問而互(hu)不(bu)影響
二:如何存放線程本地變量?
在ThreadLocal類中(zhong)有一個(ge)ThreadLocalMap, 用于存(cun)放每(mei)一個(ge)線程(cheng)的變量副(fu)本,Map中(zhong)元素的key為線程(cheng)對象(xiang),value為對應線程(cheng)的變量副(fu)本。
另外,說ThreadLocal使(shi)得各線(xian)程能夠保持各自獨立的(de)(de)一(yi)(yi)個(ge)(ge)對(dui)(dui)(dui)象(xiang),并不是(shi)通過ThreadLocal.set()來實(shi)現的(de)(de),而是(shi)通過每個(ge)(ge)線(xian)程中的(de)(de)new 對(dui)(dui)(dui)象(xiang) 的(de)(de)操作來創建(jian)的(de)(de)對(dui)(dui)(dui)象(xiang),每個(ge)(ge)線(xian)程創建(jian)一(yi)(yi)個(ge)(ge),不是(shi)什么(me)對(dui)(dui)(dui)象(xiang)的(de)(de)拷貝或副本(ben)。通過ThreadLocal.set()將這個(ge)(ge)新(xin)創建(jian)的(de)(de)對(dui)(dui)(dui)象(xiang)的(de)(de)引用(yong)保存(cun)到各線(xian)程的(de)(de)自己的(de)(de)一(yi)(yi)個(ge)(ge)map中,每個(ge)(ge)線(xian)程都有這樣一(yi)(yi)個(ge)(ge)map,執行ThreadLocal.get()時,各線(xian)程從自己的(de)(de)map中取(qu)出(chu)放進去(qu)的(de)(de)對(dui)(dui)(dui)象(xiang),因此(ci)取(qu)出(chu)來的(de)(de)是(shi)各自自己線(xian)程中的(de)(de)對(dui)(dui)(dui)象(xiang),ThreadLocal實(shi)例(li)是(shi)作為map的(de)(de)key來使(shi)用(yong)的(de)(de)。
如果ThreadLocal.set()進去的(de)東(dong)西本來就是(shi)多(duo)個線(xian)程(cheng)共享的(de)同一個對象,那么多(duo)個線(xian)程(cheng)的(de)ThreadLocal.get()取得(de)的(de)還是(shi)這個共享對象本身(shen),還是(shi)有(you)并發訪問(wen)問(wen)題。
歸納了兩點:
1。每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
2。將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然后在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
ThreadLocal的(de)(de)應用場合(he),我覺得最適(shi)合(he)的(de)(de)是按線(xian)程(cheng)多實例(每個(ge)線(xian)程(cheng)對應一個(ge)實例)的(de)(de)對象(xiang)(xiang)的(de)(de)訪問,并(bing)且這個(ge)對象(xiang)(xiang)很多地方(fang)都要用到。
三、源碼解讀
很多(duo)人(ren)對ThreadLocal存在一定的誤解,說ThreadLocal中有(you)一個全(quan)局的Map,set時執行(xing)map.put(Thread.currentThread(), value),get和remove時也(ye)同理
首先看一下ThreadLocal的API:
- get():返回此線程局部變量的當前線程副本中的值。
- protected T initialValue(): 返回此線程局部變量的當前線程的“初始值”。
- void remove(): 移除此線程局部變量當前線程的值。
- void set(T value): 將此線程局部變量的當前線程副本中的值設置為指定值。
set方(fang)法:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 獲(huo)取當(dang)前線(xian)程對象
Thread t = Thread.currentThread();
// 獲取當(dang)前線程本地變量Map
ThreadLocalMap map = getMap(t);
// map不為空
if (map != null)
// 存值
map.set(this, value);
else
// 創建一個當(dang)前(qian)線程本地變量Map
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
// 獲(huo)取當前線程的本(ben)地變量Map
return t.threadLocals;
}
這(zhe)(zhe)(zhe)里(li)注意,ThreadLocal中(zhong)是(shi)(shi)有一(yi)個Map,但(dan)這(zhe)(zhe)(zhe)個Map不(bu)是(shi)(shi)我們平(ping)時使(shi)用(yong)的(de)(de)(de)(de)Map,而是(shi)(shi)ThreadLocalMap,ThreadLocalMap是(shi)(shi)ThreadLocal的(de)(de)(de)(de)一(yi)個內(nei)部類,不(bu)對(dui)外使(shi)用(yong)的(de)(de)(de)(de)。當使(shi)用(yong)ThreadLocal存(cun)值時,首先是(shi)(shi)獲取到(dao)當前(qian)線(xian)(xian)程(cheng)對(dui)象(xiang),然后獲取到(dao)當前(qian)線(xian)(xian)程(cheng)本地(di)變(bian)量(liang)Map,最后將當前(qian)使(shi)用(yong)的(de)(de)(de)(de)ThreadLocal和傳入的(de)(de)(de)(de)值放(fang)到(dao)Map中(zhong),也就是(shi)(shi)說ThreadLocalMap中(zhong)存(cun)的(de)(de)(de)(de)值是(shi)(shi)[ThreadLocal對(dui)象(xiang), 存(cun)放(fang)的(de)(de)(de)(de)值],這(zhe)(zhe)(zhe)樣做的(de)(de)(de)(de)好處(chu)是(shi)(shi),每(mei)個線(xian)(xian)程(cheng)都(dou)對(dui)應一(yi)個本地(di)變(bian)量(liang)的(de)(de)(de)(de)Map,所以一(yi)個線(xian)(xian)程(cheng)可(ke)以存(cun)在多個線(xian)(xian)程(cheng)本地(di)變(bian)量(liang)。
get方(fang)法:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
// 如果值為(wei)空,則返(fan)回初始值
return setInitialValue();
}
看了之前set方法的分析,get方法也同理,需要說明的是,如(ru)果(guo)沒有進行過set操(cao)作,那從(cong)ThreadLocalMap中拿到(dao)的(de)值就是null。
四、使用場景
ThreadLocal對象通常用于防止對(dui)可變的單實例變量或全局變量進行共享(xiang)。
當一個(ge)類中(zhong)使用了(le)static成(cheng)(cheng)員(yuan)變量(liang)的(de)時(shi)候,一定要(yao)(yao)多問問自己,這個(ge)static成(cheng)(cheng)員(yuan)變量(liang)需要(yao)(yao)考(kao)(kao)慮線程(cheng)安全(quan)嗎?也就(jiu)是說,多個(ge)線程(cheng)需要(yao)(yao)獨(du)享自己的(de)static成(cheng)(cheng)員(yuan)變量(liang)嗎?如果需要(yao)(yao)考(kao)(kao)慮,不妨使用ThreadLocal。
ThreadLocal的(de)(de)主要應用(yong)(yong)場景(jing)為(wei)多線程(cheng)多實(shi)例(每個(ge)(ge)(ge)線程(cheng)對應一(yi)個(ge)(ge)(ge)實(shi)例)的(de)(de)對象(xiang)的(de)(de)訪(fang)問,并(bing)且(qie)這個(ge)(ge)(ge)對象(xiang)很多地方都要用(yong)(yong)到。例如(ru):同(tong)一(yi)個(ge)(ge)(ge)網(wang)站登錄用(yong)(yong)戶(hu),每個(ge)(ge)(ge)用(yong)(yong)戶(hu)服(fu)務器會(hui)為(wei)其開一(yi)個(ge)(ge)(ge)線程(cheng),每個(ge)(ge)(ge)線程(cheng)中創建一(yi)個(ge)(ge)(ge)ThreadLocal,里(li)面存用(yong)(yong)戶(hu)基本信息等,在很多頁面跳轉時,會(hui)顯示用(yong)(yong)戶(hu)信息或者(zhe)得(de)到用(yong)(yong)戶(hu)的(de)(de)一(yi)些信息等頻繁(fan)操作,這樣多線程(cheng)之間并(bing)沒有聯系而(er)且(qie)當前線程(cheng)也可以及(ji)時獲取(qu)想要的(de)(de)數據。
五、注意事項

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。
其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
但(dan)是這(zhe)些被動的預(yu)防措施并不能(neng)保證不會內(nei)存泄(xie)漏:
- 使用
static的ThreadLocal,延長了ThreadLocal的生命周期,可能導致的內存泄漏(參考)。 - 分配使用了
ThreadLocal又不再調用get(),set(),remove()方法,那么就會導致內存泄漏。
為什么使用(yong)弱引用(yong)
從表面上看內存泄漏的根源在于使用了弱引用。網上的文章大多著重分析ThreadLocal使用了弱引用會導致內存泄漏,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強引用?
我們先來看(kan)看(kan)官(guan)方文檔的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了應對非常大和長時間的用(yong)途(tu),哈希表使用(yong)弱引用(yong)的 key。
下面我們分兩(liang)種情況討論(lun):
- key 使用強引用:引用的
ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。 - key 使用弱引用:引用的
ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
比較兩種情況,我們可以發現:由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
因此,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因后果,那么怎么避免內存泄漏呢?
在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。
參考資料(liao):
