ThreadLocal 是 Java 並發編程中「線程隔離變量」的利器,廣泛用於 Spring 事務管理、請求上下文傳遞、數據庫連接持有等場景。但它也是記憶體洩漏高發區、線程池複用陷阱重災區。本文從原理、源碼、手寫實現、四大誤區、最佳實踐五個維度徹底講透。
許多人以為 ThreadLocal 存儲值,實際上 值存儲在 Thread 實例的 threadLocals 字段(ThreadLocalMap)中。ThreadLocal 只是作為 Key 的工具類。
// Thread.java 核心字段
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocalMap 結構:Entry 繼承 WeakReference<ThreadLocal<?>>
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }
}
// 線性探測解決哈希衝突,初始容量 16,負載因子 2/3
}
set(T value):獲取當前線程 ThreadLocalMap → 以 this (ThreadLocal 實例) 為 Key 存入 Entry(弱引用 Key + 強引用 Value)。get():獲取 Map → 以 this 為 Key 查找 Entry → 返回 value。若無則調用 initialValue() 初始化並存入。remove():獲取 Map → 以 this 為 Key 移除 Entry → 關鍵:防止記憶體洩漏。public class SimpleThreadLocal<T> {
// 模擬 Thread 中的 Map:Key=Thread, Value=T
private final Map<Thread, T> map = new ConcurrentHashMap<>();
protected T initialValue() { return null; }
public T get() {
Thread t = Thread.currentThread();
T value = map.get(t);
if (value == null && !map.containsKey(t)) {
value = initialValue();
map.put(t, value);
}
return value;
}
public void set(T value) {
map.put(Thread.currentThread(), value);
}
public void remove() {
map.remove(Thread.currentThread());
}
}
關鍵差異:真實版 Key 是 ThreadLocal 實例(弱引用),支持同一線程多個 ThreadLocal 變量;手寫版 Key 是 Thread,簡化模型。
正確:它是輔助工具,利用線程自帶的 Map 實現隔離。線程才是變量的真正擁有者。
真相:ThreadLocal 綁定線程,而非 Session。Web 容器使用線程池,同一線程服務不同請求(不同用戶)。若不 remove(),用戶 A 的數據將洩漏給複用該線程的用戶 B。
場景:Tomcat 預設 200 線程池,併發 1000 用戶 → 線程複用率極高 → 必須在 Filter 最後 remove()。
真相:線程池複用線程 → ThreadLocal 實例常駐堆內存(通常為 static final)→ 線程的 Map 持有舊值 → 必須手動清理。
後果:類依賴 ThreadLocal 隱式上下文 → 單元測試困難(需手動 set/remove)、代碼耦合、遞歸調用覆蓋值、異步線程丟失上下文。
原則:僅用於框架級基礎設施(事務、連接、追蹤 ID),業務代碼傳參更清晰。
// 洩漏鏈路:
Thread (GC Root)
→ threadLocals (ThreadLocalMap)
→ Entry (Key: WeakReference(ThreadLocal), Value: 大對象)
→ Value 強引用大對象 → 無法 GC
// 條件:
1. ThreadLocal 實例不可達(如類卸載、動態創建不再引用)
2. 但 Thread 仍存活(線程池長期運行)
3. 未調用 remove() → Entry 留在 Map 中
4. Key 弱引用被 GC 回收 → Entry.key = null
5. Value 強引用仍存在 → 佔用堆內存,直到線程結束或 rehash 清理
JDK 8+ 自救機制:get()/set()/remove() 會觸發 expungeStaleEntry() 掃描清理 key == null 的 Entry。但不主動調用就不會清理,不能依賴。
private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>(); 確保 Key 全局唯一、生命週期長。CONTEXT.set(userContext);
try {
// 業務邏輯
} finally {
CONTEXT.remove(); // 關鍵!防洩漏、防污染
}
TransmittableThreadLocal (Alibaba TTL) 或手動在提交任務前 get() 並在任務中 set()。threadLocals 大小,發現異常增長報警。| 場景 | 作用 | 關鍵點 |
|---|---|---|
| Spring 事務管理 | 綁定 Connection/Session 到當前線程 | DataSourceUtils、TransactionSynchronizationManager |
| 請求鏈路追蹤 | TraceId 透傳,日誌關聯 | Filter 入口生成/獲取,出口 remove,MDC 配合 |
| 用戶身份上下文 | 避免層層傳參 User 對象 | 僅限同步調用鏈,異步需額外處理 |
| SimpleDateFormat 線程安全 | 每線程獨享實例 | JDK 8+ 建議用 DateTimeFormatter (線程安全) |
| 數據庫分片路由 | 當前線程綁定分片 Key | ShardingSphere、MyBatis-Plus 實現 |
ThreadLocal 本質是「以空間換時間、以線程隔離替代參數傳遞」。理解 ThreadLocalMap 弱引用 Key、強引用 Value、線程池複用特性,是用好它、避坑的核心。記住:不用時必須 remove(),業務代碼優先傳參,框架代碼謹慎使用。