## 43 ThreadLocal 源碼解析
## 引導語
ThreadLocal 提供了一種方式,讓在多線程環境下,每個線程都可以擁有自己獨特的數據,并且可以在整個線程執行過程中,從上而下的傳遞。
### 1 用法演示
可能很多同學沒有使用過 ThreadLocal,我們先來演示下 ThreadLocal 的用法,demo 如下:
```
/** * ThreadLocal 中保存的數據是 Map */ static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>(); @Test public void testThread() { // 從上下文中拿出 Map Map<String, String> contextMap = context.get(); if (CollectionUtils.isEmpty(contextMap)) { contextMap = Maps.newHashMap(); } contextMap.put("key1", "value1"); context.set(contextMap); log.info("key1,value1被放到上下文中"); // 從上下文中拿出剛才放進去的數據 getFromComtext(); } private String getFromComtext() { String value1 = context.get().get("key1"); log.info("從 ThreadLocal 中取出上下文,key1 對應的值為:{}", value1); return value1; } //運行結果: demo.ninth.ThreadLocalDemo - key1,value1被放到上下文中 demo.ninth.ThreadLocalDemo - 從 ThreadLocal 中取出上下文,key1 對應的值為:value1
```
從運行結果中可以看到,key1 對應的值已經從上下文中拿到了。
getFromComtext 方法是沒有接受任何入參的,通過 context.get().get(“key1”) 這行代碼就從上下文中拿到了 key1 的值,接下來我們一起來看下 ThreadLocal 底層是如何實現上下文的傳遞的。
### 2 類結構
#### 2.1 類泛型
ThreadLocal 定義類時帶有泛型,說明 ThreadLocal 可以儲存任意格式的數據,源碼如下:
```
public class ThreadLocal<T> {}
```
#### 2.2 關鍵屬性
ThreadLocal 有幾個關鍵屬性,我們一一看下:
```
// threadLocalHashCode 表示當前 ThreadLocal 的 hashCode,用于計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置 private final int threadLocalHashCode = nextHashCode(); // 計算 ThreadLocal 的 hashCode 值(就是遞增) private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // static + AtomicInteger 保證了在一臺機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的 // 被 static 修飾非常關鍵,因為一個線程在處理業務的過程中,ThreadLocalMap 是會被 set 多個 ThreadLocal 的,多個 ThreadLocal 就依靠 threadLocalHashCode 進行區分 private static AtomicInteger nextHashCode = new AtomicInteger();
```
還有一個重要屬性:ThreadLocalMap,當一個線程有多個 ThreadLocal 時,需要一個容器來管理多個 ThreadLocal,ThreadLocalMap 的作用就是這個,管理線程中多個 ThreadLocal。
#### 2.2.1 ThreadLocalMap
ThreadLocalMap 本身就是一個簡單的 Map 結構,key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底層是數組的數據結構,源碼如下:
```
static class ThreadLocalMap { // 數組中的每個節點值,WeakReference 是弱引用,當沒有引用指向時,會直接被回收 static class Entry extends WeakReference<ThreadLocal<?>> { // 當前 ThreadLocal 關聯的值 Object value; // WeakReference 的引用 referent 就是 ThreadLocal Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 數組的初始化大小 private static final int INITIAL_CAPACITY = 16; // 存儲 ThreadLocal 的數組 private Entry[] table; // 擴容的閾值,默認是數組大小的三分之二 private int threshold; }
```
從源碼中看到 ThreadLocalMap 其實就是一個簡單的 Map 結構,底層是數組,有初始化大小,也有擴容閾值大小,數組的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。
### 3 ThreadLocal 是如何做到線程之間數據隔離的
ThreadLocal 是線程安全的,我們可以放心使用,主要因為是 ThreadLocalMap 是線程的屬性,我們看下線程 Thread 的源碼,如下:

從上圖中,我們可以看到 ThreadLocals.ThreadLocalMap 和 InheritableThreadLocals.ThreadLocalMap 分別是線程的屬性,所以每個線程的 ThreadLocals 都是隔離獨享的。
父線程在創建子線程的情況下,會拷貝 inheritableThreadLocals 的值,但不會拷貝 threadLocals 的值,源碼如下:

從上圖中我們可以看到,在線程創建時,會把父線程的 inheritableThreadLocals 屬性值進行拷貝。
### 4 set 方法
set 方法的主要作用是往當前 ThreadLocal 里面 set 值,假如當前 ThreadLocal 的泛型是 Map,那么就是往當前 ThreadLocal 里面 set map,源碼如下:
```
// set 操作每個線程都是串行的,不會有線程安全的問題 public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 當前 thradLocal 之前有設置值,直接設置,否則初始化 if (map != null) map.set(this, value); // 初始化ThreadLocalMap else createMap(t, value); }
```
代碼邏輯比較清晰,我們在一起來看下 ThreadLocalMap.set 的源碼,如下:
```
private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 計算 key 在數組中的下標,其實就是 ThreadLocal 的 hashCode 和數組大小-1取余 int i = key.threadLocalHashCode & (len-1); // 整體策略:查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置 // 這種解決 hash 沖突的策略,也導致了其在 get 時查找策略有所不同,體現在 getEntryAfterMiss 中 for (Entry e = tab[i]; e != null; // nextIndex 就是讓在不超過數組長度的基礎上,把數組的索引位置 + 1 e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 找到內存地址一樣的 ThreadLocal,直接替換
if (k == key) { e.value = value; return; } // 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 當前 i 位置是無值的,可以被當前 thradLocal 使用 tab[i] = new Entry(key, value); int sz = ++size; // 當數組大小大于等于擴容閾值(數組大小的三分之二)時,進行擴容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
```
上面源碼我們注意幾點:
1. 是通過遞增的 AtomicInteger 作為 ThreadLocal 的 hashCode 的;
2. 計算數組索引位置的公式是:hashCode 取模數組大小,由于 hashCode 不斷自增,所以不同的 hashCode 大概率上會計算到同一個數組的索引位置(但這個不用擔心,在實際項目中,ThreadLocal 都很少,基本上不會沖突);
3. 通過 hashCode 計算的索引位置 i 處如果已經有值了,會從 i 開始,通過 +1 不斷的往后尋找,直到找到索引位置為空的地方,把當前 ThreadLocal 作為 key 放進去。
好在日常工作中使用 ThreadLocal 時,常常只使用 1~2 個 ThreadLocal,通過 hash 計算出重復的數組的概率并不是很大。
set 時的解決數組元素位置沖突的策略,也對 get 方法產生了影響,接著我們一起來看一下 get 方法。
### 5 get 方法
get 方法主要是從 ThreadLocalMap 中拿到當前 ThreadLocal 儲存的值,源碼如下:
```
public T get() { // 因為 threadLocal 屬于線程的屬性,所以需要先把當前線程拿出來 Thread t = Thread.currentThread(); // 從線程中拿到 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 從 map 中拿到 entry,由于 ThreadLocalMap 在 set 時的 hash 沖突的策略不同,導致拿的時候邏輯也不太一樣 ThreadLocalMap.Entry e = map.getEntry(this); // 如果不為空,讀取當前 ThreadLocal 中保存的值 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 否則給當前線程的 ThreadLocal 初始化,并返回初始值 null return setInitialValue(); }
```
接著我們來看下 ThreadLocalMap 的 getEntry 方法,源碼如下:
```
// 得到當前 thradLocal 對應的值,值的類型是由 thradLocal 的泛型決定的 // 由于 thradLocalMap set 時解決數組索引位置沖突的邏輯,導致 thradLocalMap get 時的邏輯也是對應的 // 首先嘗試根據 hashcode 取模數組大小-1 = 索引位置 i 尋找,找不到的話,自旋把 i+1,直到找到索引位置不為空為止 private Entry getEntry(ThreadLocal<?> key) {
// 計算索引位置:ThreadLocal 的 hashCode 取模數組大小-1 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // e 不為空,并且 e 的 ThreadLocal 的內存地址和 key 相同,直接返回,否則就是沒有找到,繼續通過 getEntryAfterMiss 方法找 if (e != null && e.get() == key) return e; else // 這個取數據的邏輯,是因為 set 時數組索引位置沖突造成的 return getEntryAfterMiss(key, i, e); } // 自旋 i+1,直到找到為止 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的 while (e != null) { ThreadLocal<?> k = e.get(); // 內存地址一樣,表示找到了 if (k == key) return e; // 刪除沒用的 key if (k == null) expungeStaleEntry(i); // 繼續使索引位置 + 1 else i = nextIndex(i, len); e = tab[i]; } return null; }
```
get 邏輯源碼中注釋已經寫的很清楚了,我們就不重復說了。
### 6 擴容
ThreadLocalMap 中的 ThreadLocal 的個數超過閾值時,ThreadLocalMap 就要開始擴容了,我們一起來看下擴容的邏輯:
```
//擴容 private void resize() { // 拿出舊的數組 Entry[] oldTab = table; int oldLen = oldTab.length; // 新數組的大小為老數組的兩倍 int newLen = oldLen * 2; // 初始化新數組 Entry[] newTab = new Entry[newLen]; int count = 0; // 老數組的值拷貝到新數組上 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { // 計算 ThreadLocal 在新數組中的位置 int h = k.threadLocalHashCode & (newLen - 1); // 如果索引 h 的位置值不為空,往后+1,直到找到值為空的索引位置 while (newTab[h] != null) h = nextIndex(h, newLen); // 給新數組賦值 newTab[h] = e; count++;
} } } // 給新數組初始化下次擴容閾值,為數組長度的三分之二 setThreshold(newLen); size = count; table = newTab; }
```
源碼注解也比較清晰,我們注意兩點:
1. 擴容后數組大小是原來數組的兩倍;
2. 擴容時是絕對沒有線程安全問題的,因為 ThreadLocalMap 是線程的一個屬性,一個線程同一時刻只能對ThreadLocalMap 進行操作,因為同一個線程執行業務邏輯必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。
### 7 總結
ThreadLocal 是非常重要的 API,我們在寫一個中間件的時候經常會用到,比如說流程引擎中上下文的傳遞,調用鏈ID的傳遞等等,非常好用,但坑也很多。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題