ThreadLocal 是 Java 並發編程中獨特的「線程局部變量」機制。它不解決共享訪問衝突,而是為每個線程提供獨立的變量副本,從根本上避免了同步開銷。本文深入剖析其核心原理、典型應用場景、記憶體洩漏根因與最佳實踐。
// Thread 類中
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// ThreadLocalMap 內部
static class ThreadLocalMap {
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
// ...
}
關鍵點:
Thread 持有 ThreadLocalMap,Map 的 Key 是 ThreadLocal 實例的弱引用。ThreadLocal 實例被 GC 回收,Key 變 null,Entry 成為「陰影條目」(stale entry),Value 無法通過 Key 訪問,但仍被 Map 強引用持有 → 記憶體洩漏。public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) map.set(this, value);
else createMap(t, value);
}
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;
}
return setInitialValue(); // 呼叫 initialValue()
}
protected T initialValue() { return null; } // 可覆寫提供預設值
public class DbContextHolder {
private static final ThreadLocal CONNECTION_HOLDER =
ThreadLocal.withInitial(() -> DataSourceUtils.getConnection());
public static Connection getConnection() { return CONNECTION_HOLDER.get(); }
public static void clear() { CONNECTION_HOLDER.remove(); }
}
// Filter/Interceptor 中
try {
// 業務邏輯自動複用同一連線
service.doBusiness();
} finally {
DbContextHolder.clear(); // 必須清理!
}
// TransactionSynchronizationManager.java 簡化版
private static final ThreadLocal
// Looper.java 核心片段
static final ThreadLocal sThreadLocal = new ThreadLocal<>();
public static void prepare() {
if (sThreadLocal.get() != null) throw new RuntimeException("Only one Looper per thread");
sThreadLocal.set(new Looper());
}
public static Looper myLooper() { return sThreadLocal.get(); }
每個線程最多一個 Looper,ThreadLocal 完美契合。
public class RequestContext {
private static final ThreadLocal CONTEXT = new ThreadLocal<>();
public static void set(RequestInfo info) { CONTEXT.set(info); }
public static RequestInfo get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}
// 攔截器
RequestContext.set(new RequestInfo(requestId, userId, traceId));
try { chain.doFilter(req, res); } finally { RequestContext.clear(); }
// SimpleDateFormat 非線程安全
private static final ThreadLocal DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) { return DATE_FORMAT.get().format(date); }
public static Date parse(String str) throws ParseException { return DATE_FORMAT.get().parse(str); }
ThreadLocal 實例不再被強引用(如方法局部變量、類卸載)。remove() 清理 Value。get/set 掃描效能下降。get()、set()、remove() 時會啟發式清理部分 stale entry。resize() 擴容時全量清理。private static final ThreadLocal HOLDER = ThreadLocal.withInitial(...);
try {
HOLDER.set(value);
// 業務邏輯
} finally {
HOLDER.remove(); // 關鍵!
}
public class AutoCleanThreadLocal extends ThreadLocal {
@Override public T get() {
T val = super.get();
// 可選:結合註解/攔截器實現自動清理
return val;
}
}
// 或使用 Spring RequestScope、InheritableThreadLocal 等成熟方案
// 父線程
ThreadLocal tl = new InheritableThreadLocal<>();
tl.set("parent-value");
new Thread(() -> {
System.out.println(tl.get()); // "parent-value" 子線程可見
}).start();
注意:只在線程創建時複製快照,後續父線程修改不影響子線程,反之亦然。線程池中慎用(線程非父子關係)。
| 方案 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| ThreadLocal | 線程封閉、請求上下文、非線程安全對象復用 | 無鎖、高效能、代碼非侵入 | 洩漏風險、隱式依賴、線程池需清理 |
| synchronized/Lock | 真正共享狀態修改 | 語義清晰、安全 | 爭用開銷、死鎖風險 |
| 方法參數傳遞 | 所有場景 | 顯式、無副作用、可測試 | 參數傳遞地獄、侵入性強 |
| ThreadLocal + 參數 | 框架層透傳,業務層顯式 | 平衡 | 需約定俗成 |
// 1. 遍歷當前線程所有 ThreadLocal(反射,僅除錯用)
Field threadLocals = Thread.class.getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
ThreadLocalMap map = (ThreadLocalMap) threadLocals.get(Thread.currentThread());
// 遍歷 map.table 陣列檢查 Entry
// 2. Java Agent 監控 ThreadLocal 洩漏
// 3. 使用 -XX:+HeapDumpOnOutOfMemoryError 分析 heap 中的 ThreadLocalMap
ThreadLocal 是「以空間換時間、以隔離換同步」的利器:
static final 定義 + try-finally remove() 清理。正確使用 ThreadLocal,能讓並發代碼更簡潔、高效;濫用或疏忽清理,則是定時炸彈。