## 18 線程作用域內共享變量—深入解析ThreadLocal
> 一個人追求的目標越高,他的才力就發展得越快,對社會就越有益。
> ——高爾基
本章前面三節分別講解了線程同步的三種方式。無論是輕量級的Atomic、volatile,還是synchronized,其實都是采用同步的方式解決了線程安全問題。本節我們將介紹另外一種解決線程安全問題的思路,線程封閉。
如果你有一個全局共享的變量,那么多線程并發的時候,對這個共享變量的訪問是不安全的。方法內的局部變量是線程安全的,由于每個線程都會有自己的副本。也就是說局部變量被封閉在線程內部,其它線程無法訪問(引用型有所區別)。那么有沒有作用域介于兩者之間,既能保證線程安全,又不至于只局限于方法內部的方式呢?答案是肯定的,我們使用ThreadLocal就可以做到這一點。ThreadLocal變量的作用域是為線程,也就是說線程內跨方法共享。例如某個對象的方法A對threadLocal變量賦值,在同一個線程中的另外一個對象的方法B能夠讀取到該值。因為作用域為同一個線程,那么自然就是線程安全的。但是需要注意的是,如果threadLocal存儲的是共享變量的引用,那么同樣會有線程安全問題。
## 1、ThreadLocal 的使用場景
ThreadLocal的特性決定了它的使用場景。由于ThreadLocal中存儲的變量是線程隔離的,所以一般在以下情況使用ThreadLocal:
1、存儲需要在線程隔離的數據。比如線程執行的上下文信息,每個線程是不同的,但是對于同一個線程來說會共享同一份數據。Spring MVC的 RequestContextHolder 的實現就是使用了ThreadLocal;
2、跨層傳遞參數。層次劃分在軟件設計中十分常見。層次劃分后,體現在代碼層面就是每層負責不同職責,一個完整的業務操作,會由一系列不同層的類的方法調用串起來完成。有些時候第一層獲得的一個變量值可能在第三層、甚至更深層的方法中才會被使用。如果我們不借助ThreadLocal,就只能一層層地通過方法參數進行傳遞。使用ThreadLocal后,在第一層把變量值保存到ThreadLocal中,在使用的層次方法中直接從ThreadLocal中取出,而不用作為參數在不同方法中傳來傳去。不過千萬不要濫用ThreadLocal,它的本意并不是用來跨方法共享變量的。結合第一種情況,我們放入ThreadLocal跨層傳遞的變量一般也是具有上下文屬性的。比如用戶的信息等。這樣我們在AOP處理異常或者其他操作時可以很方便地獲取當前登錄用戶的信息。
## 2、如何使用 ThreadLocal
ThreadLocal使用起來非常簡單,我們先看一個簡單的例子。
可以看到每個線程為同一個ThreadLocal對象set不同的值,但各個線程打印出來的依舊是自己保存進去的值,并沒有被其它線程所覆蓋。
一般來說,在實踐中,我們會把ThreadLocal對象聲名為static final,作為私有變量封裝到自定義的類中。另外提供static的set和get方法。如下面的代碼:
~~~java
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的內存泄漏,具體原因下文中會有講解。
使用的時候可以在任何方法的任何位置調用OperationInfoRecorder的set或者get方法,保存和取出。如下面代碼:
~~~
OperationInfoRecorder.set(operationInfoDTO)
OperationInfoRecorder.get()
~~~
## 3、ThreadLocal源代碼解析
學習到這里,你一定很好奇ThreadLocal是如何做到多個線程對同一個對象set操作,但只會get出自己set進去的值呢?這個現象有點違背我們的認知。接下來我們就從set方法入手,來看看ThreadLocal的源代碼:
~~~java
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能夠保存多個線程的變量值,那么它一定是借助容器來實現的。
這個map不是一般的map,可以看到它是通過當前線程對象獲取到的ThreadLocalMap。看到這里應該看出些端倪,這個map其實是和Thread綁定的。接下來我們看getMap方法的代碼:
~~~java
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
~~~
原來這個ThreadLocal就存方法Thread對象上。下面我們看看Thread中的相關代碼:
~~~java
/* 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。
看到這里,我們大至已經理清了ThreadLocal和Thread的關系,我們看下圖:

我們接下來分析get方法,代碼如下:
~~~java
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對象,而每個班主任是一個線程,記錄的評選結果的小本子就是ThreadLocalMap對象。每個班主任會在自己的小本子上記錄下評選結果,比如說一班班主任記錄:道德標兵:小明,學習標兵:小紅。二班班主任記錄:道德標兵:小趙,學習標兵:小巖。通過這個例子大家應該很清楚ThreadLocal的原理了。
ThreadLocal的設計真的非常巧妙,看似自己保存了每個線程的變量副本,其實每個線程的變量副本是保存在線程對象中,那么自然就線程隔離了。如此分析起來,是不是有一種ThreadLocal沒做什么事情,卻搶了頭功的感覺?其實不然。Thread對象中用來保存變量副本的ThreadLocalMap的定義就在ThreadLocal中。我們接下來分析ThreadLocalMap的源代碼。
## 4、ThreadLocalMap分析
ThreadLocalMap是ThreadLocal的靜態內部類,我們單獨一小節來講解它。ThreadLocalMap的功能其實是和HashMap類似的,但是為什么不直接使用HashMap呢?˙在ThreadLocalMap中使用WeakReference包裝后的ThreadLocal對象作為key,也就是說這里對ThreadLocal對象為弱引用。當ThreadLocal對象在ThreadLocalMap引用之外,再無其他引用的時候能夠被垃圾回收。如下面代碼所示:
~~~java
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一直不終結的話,value對象就會一直駐留在內存中,直至Thread被銷毀后,才會被回收。這就是ThreadLocal引起內存泄漏問題。
而ThreadLocalMap在設計的時候也考慮到這一點,在get和set的時候,會把遇到的key為null的entry清理掉。不過這樣做也不能100%保證能夠清理干凈。我們可以通過以下兩種方式來避免這個問題:
1、把ThreadLocal對象聲明為static,這樣ThreadLocal成為了類變量,生命周期不是和對象綁定,而是和類綁定,延長了聲明周期,避免了被回收;
2、在使用完ThreadLocal變量后,手動remove掉,防止ThreadLocalMap中Entry一直保持對value的強引用。導致value不能被回收。
## 4、總結
通過本節學習,我們掌握了ThreadLocal 的原理和其使用場景。絕大多數情況下,ThreadLocal用于存儲和線程相關的上下文信息,也就是線程共享的信息,便于同一線程的不同方法中取值,而不用作為方法參數層層傳遞。
}
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語