ThreadLocal 的设计模式
ThreadLocal 是 Java 并发编程中实现**线程封闭(Thread Confinement)**的核心工具。本文将从原理到实践,系统性地讲解 ThreadLocal 的设计哲学、内部机制、使用模式以及跨线程传递方案。
原理篇:ThreadLocal 的内部机制
核心设计理念:为什么不用 Map<Thread, Value>?
很多人初次设计线程本地存储时,会想到用一个全局的 Map<Thread, Value> 来存储每个线程的数据。但这种设计有致命缺陷:Thread 对象会被 Map 强引用,导致线程无法被 JVM 回收,造成严重的内存泄漏。
ThreadLocal 采用了相反的设计:让 Thread 持有 Map,而不是让 Map 持有 Thread。每个 Thread 内部都有一个 ThreadLocalMap,用于存储该线程的所有线程本地变量。这样设计的好处是:
- 线程销毁时,ThreadLocalMap 随之销毁,数据自动清理
- ThreadLocal 对象可以被显式管理(如声明为静态变量)
- 线程内部的存储容器是隐式的,由线程自己管理
引用关系结构
1 | |
一个 ThreadLocal 变量的"副本"实际上是分散存储在多个线程的 ThreadLocalMap 中的。每个线程的 ThreadLocalMap 中都有一个 Entry,以同一个 ThreadLocal 对象为 key,但 value 是各自独立的。
静态变量场景的引用关系:
1 | |
四大核心原则
原则1:操作的本质
当我们调用 ThreadLocal.get() 或 ThreadLocal.set() 时,实际上是在操作当前线程内部的 ThreadLocalMap。ThreadLocal 对象本身只是一个"访问入口",真正的数据存储在各个线程的隐藏 Map 中。
graph TB
subgraph "方法区"
TL[ThreadLocal 静态变量<br/>static ThreadLocal]
style TL fill:#e1f5ff
end
subgraph "Thread-1 内部"
T1[Thread-1]
TLM1[ThreadLocalMap]
E1[Entry]
V1[value: data1]
T1 -->|强引用| TLM1
TLM1 -->|强引用| E1
E1 -.->|key弱引用| TL
E1 -->|强引用| V1
end
subgraph "Thread-2 内部"
T2[Thread-2]
TLM2[ThreadLocalMap]
E2[Entry]
V2[value: data2]
T2 -->|强引用| TLM2
TLM2 -->|强引用| E2
E2 -.->|key弱引用| TL
E2 -->|强引用| V2
end
TL -.->|"get()/set() 操作<br/>实际访问当前线程的 Map"| TLM1
TL -.->|"get()/set() 操作<br/>实际访问当前线程的 Map"| TLM2
原则2:ThreadLocal 应该是 static 变量
ThreadLocal 应该声明为 static 变量,作为类级别的全局唯一实例。原因有三:
-
避免内存浪费:如果作为成员变量,每个对象实例都会创建新的 ThreadLocal,导致每个线程的 ThreadLocalMap 中会有多个 Entry,浪费内存且违背线程本地存储的设计初衷。我们的初衷是每个线程有一个变量的副本,而不是多个副本。
-
防止对象无法回收:如果 ThreadLocal 是成员变量,开发者会习惯性地通过
对象实例.threadLocal.get()来访问 ThreadLocal。这种使用方式会导致对象实例被外部持有引用,进而无法被 GC 回收。而如果 ThreadLocal 是 static 变量,访问方式是类.threadLocal.get(),不会持有对象实例的引用,避免了对象级别的内存泄漏。 -
Class 生命周期更长:相比之下,Class 对象通常不需要频繁回收,其生命周期与应用程序相当,因此 static 成员是可以接受的。static 声明确保全局只有一个 ThreadLocal 实例,每个线程的 Map 中只有一个对应的 Entry,既节省内存又避免了对象无法回收的问题。
graph TB
subgraph "错误做法:ThreadLocal 作为成员变量"
direction TB
subgraph "对象实例1"
O1[MyService 实例1]
TL1[ThreadLocal 实例1]
O1 -->|持有| TL1
style O1 fill:#ffcdd2
style TL1 fill:#ffcdd2
end
subgraph "对象实例2"
O2[MyService 实例2]
TL2[ThreadLocal 实例2]
O2 -->|持有| TL2
style O2 fill:#ffcdd2
style TL2 fill:#ffcdd2
end
subgraph "对象实例3"
O3[MyService 实例3]
TL3[ThreadLocal 实例3]
O3 -->|持有| TL3
style O3 fill:#ffcdd2
style TL3 fill:#ffcdd2
end
subgraph "Thread-1 的 Map"
E1A[Entry: key=TL1]
E1B[Entry: key=TL2]
E1C[Entry: key=TL3]
style E1A fill:#fff9c4
style E1B fill:#fff9c4
style E1C fill:#fff9c4
end
problem["问题:<br/>1. 每个对象实例都创建新的 ThreadLocal<br/>2. 线程的 Map 中会有多个 Entry<br/>3. 浪费内存,违背线程本地存储的设计初衷"]
style problem fill:#ffebee
end
subgraph "正确做法:ThreadLocal 作为 static 变量"
direction TB
subgraph "类级别"
TLS[static ThreadLocal<br/>全局唯一实例]
style TLS fill:#c8e6c9
end
subgraph "Thread-1 的 Map"
E2[Entry: key=TLS<br/>唯一的 Entry]
E2 -.->|key弱引用| TLS
style E2 fill:#e8f5e9
end
subgraph "Thread-2 的 Map"
E3[Entry: key=TLS<br/>唯一的 Entry]
E3 -.->|key弱引用| TLS
style E3 fill:#e8f5e9
end
benefit["优势:<br/>1. 全局只有一个 ThreadLocal 实例<br/>2. 每个线程的 Map 中只有一个对应的 Entry<br/>3. 节省内存,符合设计初衷"]
style benefit fill:#e8f5e9
end
原则3:线程生命周期决定数据生命周期
即使我们不主动调用 remove(),当线程销毁时(如普通线程执行完毕),该线程的 ThreadLocalMap 也会随之销毁,所有 value 自动被回收。这就是为什么在非线程池场景下,ThreadLocal 的内存泄漏问题不那么严重。
graph TB
subgraph "场景:普通线程执行完毕"
direction TB
subgraph "执行前"
T1A[Thread-1 运行中]
TLM1A[ThreadLocalMap]
E1A[Entry]
V1A[value: data1]
T1A -->|强引用| TLM1A
TLM1A -->|强引用| E1A
E1A -->|强引用| V1A
style T1A fill:#fff4e6
style TLM1A fill:#f0f0f0
style E1A fill:#e8f5e9
style V1A fill:#c8e6c9
end
arrow1["线程执行完毕"]
style arrow1 fill:#ffebee
subgraph "执行后"
destroyed["Thread-1 被销毁<br/>ThreadLocalMap 被销毁<br/>Entry 被销毁<br/>value 被 GC 回收"]
style destroyed fill:#ffcdd2
end
执行前 --> arrow1
arrow1 --> 执行后
end
原则4:ThreadLocal 对象的生命周期影响所有线程
如果我们将 ThreadLocal 静态变量置为 null(去除强引用),那么所有线程的 ThreadLocalMap 中对应的 Entry 的 key 都会失效(弱引用被回收)。即使线程什么都不做,只要后续有任何 get/set/remove 操作触发,这些 key 为 null 的 Entry 就会被自动清理,value 随之消失。
graph TB
subgraph "场景:ThreadLocal 静态变量被置为 null"
direction TB
subgraph "置为 null 前"
TLA[ThreadLocal 静态变量]
E1A[Thread-1 的 Entry]
E2A[Thread-2 的 Entry]
E3A[Thread-3 的 Entry]
V1A[value: data1]
V2A[value: data2]
V3A[value: data3]
E1A -.->|key弱引用| TLA
E2A -.->|key弱引用| TLA
E3A -.->|key弱引用| TLA
E1A -->|强引用| V1A
E2A -->|强引用| V2A
E3A -->|强引用| V3A
style TLA fill:#e1f5ff
style E1A fill:#e8f5e9
style E2A fill:#e8f5e9
style E3A fill:#e8f5e9
style V1A fill:#c8e6c9
style V2A fill:#c8e6c9
style V3A fill:#c8e6c9
end
arrow2["ThreadLocal = null + GC"]
style arrow2 fill:#ffebee
subgraph "置为 null 后"
TLB[ThreadLocal 对象被 GC 回收]
E1B[Thread-1 的 Entry<br/>key=null]
E2B[Thread-2 的 Entry<br/>key=null]
E3B[Thread-3 的 Entry<br/>key=null]
V1B[value: data1<br/>待清理]
V2B[value: data2<br/>待清理]
V3B[value: data3<br/>待清理]
E1B -->|强引用| V1B
E2B -->|强引用| V2B
E3B -->|强引用| V3B
style TLB fill:#ffcdd2
style E1B fill:#fff9c4
style E2B fill:#fff9c4
style E3B fill:#fff9c4
style V1B fill:#ffebee
style V2B fill:#ffebee
style V3B fill:#ffebee
end
arrow3["任意线程调用 get/set/remove"]
style arrow3 fill:#ffebee
subgraph "自动清理"
cleaned["expungeStaleEntry 触发<br/>key=null 的 Entry 被清理<br/>value 被 GC 回收"]
style cleaned fill:#c8e6c9
end
置为null前 --> arrow2
arrow2 --> 置为null后
置为null后 --> arrow3
arrow3 --> 自动清理
end
为什么 key 使用弱引用?
因为线程内部的 ThreadLocalMap 是隐式容器,由线程自己管理。如果 key 使用强引用,那么只要线程存活(如线程池场景),ThreadLocal 对象就永远无法被回收。使用弱引用后,当 ThreadLocal 对象没有外部强引用时(如静态变量被置为 null),它可以被 GC 回收,Entry 的 key 变为 null,后续的 get/set/remove 操作会自动清理这些过期的 Entry。
弱引用的特性:在垃圾回收器线程扫描内存区域时,一旦发现只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。
Stale Entry 的自动清理机制
当 ThreadLocal 对象失去强引用后,GC 会回收它,此时 Entry 中的弱引用 key 会变成 null。但 Entry 本身仍然占据 ThreadLocalMap 的槽位,value 也仍然被 Entry 强引用。这种 key 为 null 的 Entry 被称为 Stale Entry(过期条目)。
ThreadLocalMap 的 get()、set()、remove() 方法在执行过程中,会主动检测并清理这些 Stale Entry。这是通过调用 expungeStaleEntry() 方法实现的:
sequenceDiagram
participant App as 应用代码
participant TL as ThreadLocal
participant TLM as ThreadLocalMap
participant Entry as Entry[]
participant GC as GC
Note over App,GC: 阶段1:正常使用
App->>TL: threadLocal.set(value)
TL->>TLM: set(this, value)
TLM->>Entry: table[i] = new Entry(key, value)
Note over Entry: Entry.key = WeakRef(ThreadLocal)<br/>Entry.value = value(强引用)
Note over App,GC: 阶段2:ThreadLocal 失去强引用
App->>App: threadLocal = null
Note over TL: 只剩 Entry 的弱引用指向 ThreadLocal
Note over App,GC: 阶段3:GC 回收 ThreadLocal
GC->>TL: 回收 ThreadLocal 对象
Note over Entry: Entry.key.get() == null<br/>Entry.value 仍然存在(Stale Entry)
Note over App,GC: 阶段4:后续操作触发清理
App->>TL: anotherThreadLocal.get()
TL->>TLM: getEntry(this)
TLM->>TLM: 遍历 table 寻找目标 Entry
alt 遇到 Stale Entry (key == null)
TLM->>TLM: expungeStaleEntry(staleSlot)
Note over TLM: 1. 将 Entry.value 置为 null<br/>2. 将 Entry 置为 null<br/>3. rehash 后续元素
TLM->>Entry: table[staleSlot] = null
Note over Entry: value 失去引用,可被 GC 回收
end
TLM-->>TL: 返回目标 Entry 的 value
TL-->>App: 返回 value
为什么 get/set/remove 能"知道" Entry 已过期?
这是弱引用的核心特性:WeakReference.get() 方法会返回被引用的对象,但如果该对象已被 GC 回收,则返回 null。ThreadLocalMap 的 Entry 继承自 WeakReference<ThreadLocal<?>>,因此:
- 当 ThreadLocal 对象存活时:
entry.get()返回 ThreadLocal 对象。 - 当 ThreadLocal 对象被 GC 回收后:
entry.get()返回null——这就是继承 WeakReference 的好处。
graph TB
subgraph "Entry 状态判断"
CHECK["entry.get()"]
VALID["返回 ThreadLocal 对象<br/>━━━━━━━━━━━━━━━━━━━━<br/>Entry 有效<br/>正常读取/更新 value"]
STALE["返回 null<br/>━━━━━━━━━━━━━━━━━━━━<br/>Entry 过期(Stale)<br/>触发 expungeStaleEntry()"]
CHECK -->|"ThreadLocal 存活"| VALID
CHECK -->|"ThreadLocal 已被 GC"| STALE
style VALID fill:#c8e6c9
style STALE fill:#ffcdd2
end
清理的时机与局限性:
| 操作 | 是否触发清理 | 清理范围 |
|---|---|---|
get() |
是 | 遍历过程中遇到的 Stale Entry |
set() |
是 | 遍历过程中遇到的 Stale Entry + 可能触发全表扫描 |
remove() |
是 | 遍历过程中遇到的 Stale Entry |
| 无任何操作 | 否 | 这就是泄漏发生的根本原因 |
关键结论:自动清理机制是被动触发的,只有在调用 get/set/remove 时才会执行。如果线程长期存活(如线程池)且不再访问任何 ThreadLocal,那些 Stale Entry 将永远不会被清理,导致内存泄漏。
关键源码解析(JDK 8):
1 | |
ThreadLocal 核心方法调用链解析
理解 ThreadLocal 的工作原理,关键在于理清各个方法之间的调用关系。下面我们从源码层面逐一分析。
方法调用关系图
graph TB
subgraph "ThreadLocal 外部 API"
SET["ThreadLocal.set(T value)"]
GET["ThreadLocal.get()"]
REMOVE["ThreadLocal.remove()"]
end
subgraph "ThreadLocal 内部方法"
GETMAP["getMap(Thread t)"]
CREATEMAP["createMap(Thread t, T firstValue)"]
SETINITIAL["setInitialValue()"]
end
subgraph "ThreadLocalMap 内部方法"
MAPSET["ThreadLocalMap.set(ThreadLocal key, Object value)"]
GETENTRY["ThreadLocalMap.getEntry(ThreadLocal key)"]
GETMISS["getEntryAfterMiss(ThreadLocal key, int i, Entry e)"]
MAPREMOVE["ThreadLocalMap.remove(ThreadLocal key)"]
EXPUNGE["expungeStaleEntry(int staleSlot)"]
REPLACE["replaceStaleEntry(...)"]
CLEAN["cleanSomeSlots(...)"]
MAPCONSTRUCTOR["ThreadLocalMap(ThreadLocal firstKey, Object firstValue)"]
end
SET --> GETMAP
SET -->|"map != null"| MAPSET
SET -->|"map == null"| CREATEMAP
CREATEMAP --> MAPCONSTRUCTOR
GET --> GETMAP
GET -->|"map != null"| GETENTRY
GET -->|"map == null 或 entry == null"| SETINITIAL
GETENTRY -->|"hash 冲突"| GETMISS
GETMISS --> EXPUNGE
SETINITIAL --> CREATEMAP
SETINITIAL --> MAPSET
REMOVE --> GETMAP
REMOVE --> MAPREMOVE
MAPREMOVE --> EXPUNGE
MAPSET -->|"遇到 stale entry"| REPLACE
REPLACE --> EXPUNGE
MAPSET --> CLEAN
CLEAN --> EXPUNGE
style SET fill:#e1f5ff
style GET fill:#e1f5ff
style REMOVE fill:#e1f5ff
style EXPUNGE fill:#ffcdd2
核心方法源码解析
1. ThreadLocal.set(T value) —— 设置入口
1 | |
设计要点:set() 方法体现了懒加载思想——只有在首次调用 set() 时才创建 ThreadLocalMap。
2. getMap(Thread t) —— 获取线程的 Map
1 | |
设计要点:这个方法揭示了 ThreadLocal 的核心设计——Map 存储在 Thread 对象内部,而不是 ThreadLocal 对象内部。这是"让 Thread 持有 Map,而不是让 Map 持有 Thread"设计理念的直接体现。
3. createMap(Thread t, T firstValue) —— 创建 Map
1 | |
设计要点:创建 Map 时直接传入第一个键值对,避免了"先创建空 Map,再插入"的两步操作。
4. ThreadLocalMap 构造函数
1 | |
设计要点:
- 初始容量 16,与 HashMap 相同
- 使用
0x61c88647(黄金分割数)作为哈希增量,使 Entry 分布更均匀 - 扩容阈值为容量的 2/3,比 HashMap 的 0.75 更保守,减少哈希冲突
5. ThreadLocalMap.set(ThreadLocal<?> key, Object value) —— 核心设置逻辑
1 | |
设计要点:
- 使用线性探测解决哈希冲突
- 在探测过程中顺便清理 Stale Entry(
k == null的情况) cleanSomeSlots()是启发式清理,不会扫描全表
6. ThreadLocal.get() —— 获取入口
1 | |
7. getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) —— 哈希冲突时的查找
1 | |
设计要点:在查找过程中顺便清理遇到的 Stale Entry,这是"惰性清理"策略的体现。
8. ThreadLocal.remove() 和 ThreadLocalMap.remove(ThreadLocal<?> key)
1 | |
9. expungeStaleEntry(int staleSlot) —— 核心清理方法
1 | |
设计要点:
- 参数
staleSlot是已知的 Stale Entry 位置 - 不仅清理目标位置,还会继续向后扫描清理更多 Stale Entry
- 对有效 Entry 进行 rehash,确保线性探测链不断裂
- 返回值是扫描结束的位置,供调用者使用
方法调用关系总结
| 外部 API | 调用的内部方法 | 可能触发的清理方法 |
|---|---|---|
set(T) |
getMap() → ThreadLocalMap.set() 或 createMap() |
replaceStaleEntry() → expungeStaleEntry()cleanSomeSlots() → expungeStaleEntry() |
get() |
getMap() → getEntry() → getEntryAfterMiss() 或 setInitialValue() |
expungeStaleEntry() |
remove() |
getMap() → ThreadLocalMap.remove() |
expungeStaleEntry() |
核心结论:expungeStaleEntry() 是所有清理操作的最终执行者,而 get()、set()、remove() 都会在执行过程中触发它,实现"惰性清理"。
为什么 ThreadLocalMap 使用开放地址法而不是链表法?
HashMap 使用"数组 + 链表/红黑树"的结构来处理哈希冲突,而 ThreadLocalMap 却选择了开放地址法(线性探测)。这个设计选择背后有深刻的考量:
| 对比维度 | HashMap(链表法) | ThreadLocalMap(开放地址法) |
|---|---|---|
| 冲突处理 | 冲突的元素挂在同一个桶的链表上 | 冲突时向后探测下一个空槽位 |
| 内存布局 | 链表节点分散在堆中,缓存不友好 | 所有 Entry 在连续数组中,缓存友好 |
| 空间开销 | 每个节点需要额外的 next 指针 | 无额外指针开销 |
| 删除操作 | 简单的链表节点删除 | 需要 rehash 后续元素(复杂) |
ThreadLocalMap 选择开放地址法的核心原因:
-
Entry 数量通常很少:一个线程的 ThreadLocalMap 中通常只有几个到几十个 Entry(对应几个 ThreadLocal 变量),远少于 HashMap 的典型使用场景。在元素少的情况下,开放地址法的线性探测效率很高。
-
弱引用清理的需要:ThreadLocalMap 的 key 是弱引用,需要在遍历过程中发现并清理 Stale Entry。开放地址法的线性探测天然支持这种"顺便清理"的模式——在查找目标 Entry 的过程中,可以顺便清理沿途遇到的 Stale Entry。
-
缓存友好性:开放地址法将所有 Entry 存储在连续的数组中,CPU 缓存预取效果好。对于频繁访问的 ThreadLocal(如每次请求都要读取的上下文信息),缓存友好性带来的性能提升是显著的。
内存泄漏的发生机制
ThreadLocal 的内存泄漏实际上是一个条件链,任何一个环节被破坏都可能导致泄漏:
- ThreadLocal 对象被回收:当 ThreadLocal 对象没有强引用时(如静态变量被置为 null),它会被 GC 回收
- Entry 的 key 变为 null:ThreadLocalMap 中对应的 Entry 的 key(弱引用)失效
- value 仍被强引用:但 value 仍被 Entry 强引用,无法被回收(value 不是被 key 引用,而是被 Entry 引用)
- 自动清理机制:后续的 get/set/remove 操作会触发
expungeStaleEntry(),清理 key 为 null 的 Entry - 泄漏发生:如果线程长期存活(如线程池)且不再调用 get/set/remove,这些 Entry 永远不会被清理
Value 泄漏的因果链:
1 | |
结论:Value 泄漏是 Entry 泄漏的直接后果。更准确地说,是因为 Stale Entry 没有被及时清理——ThreadLocal 对象本身是可以被回收的(因为是弱引用),问题在于回收后遗留的 Stale Entry 没有被清理。
实践篇:ThreadLocal 的使用模式
基础版本:静态工具类封装
1 | |
用 Map 来取消第一层工具类的方案
这种方案使用一个 Map 来存储多种类型的上下文,但Map 容易腐化,需要谨慎使用:
1 | |
对这个 Map 的加强版本——不可变 Map 模式:
1 | |
绑定容器到线程并保存上一个状态
这是 Spring 事务管理中使用的经典模式——栈式上下文管理:
1 | |
ThreadLocal 变策略模式
Spring Security 的 SecurityContextHolder 是一个经典的策略模式实现,支持三种存储策略:
1 | |
带名字的 ThreadLocal
Spring 提供的 NamedThreadLocal,便于调试和诊断:
1 | |
跨线程传递篇:InheritableThreadLocal 与 TransmittableThreadLocal
InheritableThreadLocal 的工作原理
Thread 类的双 Map 设计
Thread 类里面其实存在两个 ThreadLocalMap:
1 | |
InheritableThreadLocal 的极简实现
令人惊讶的是,InheritableThreadLocal 只有 3 个方法,却实现了完整的父子线程值传递功能:
1 | |
设计精妙之处:通过重写 getMap() 和 createMap() 两个方法,将所有 InheritableThreadLocal 的值存储在独立的 inheritableThreadLocals Map 中,与普通 ThreadLocal 完全隔离。这样在创建子线程时,只需复制 inheritableThreadLocals,而不影响 threadLocals。
Thread.init() 方法逐行解析
线程的构造器里隐藏着继承的核心逻辑。下面是 Thread.init() 方法的逐行中文注释:
1 | |
继承流程图解
sequenceDiagram
participant Main as 主线程
participant Thread as Thread 类
participant Child as 子线程
participant ITL as InheritableThreadLocal
Note over Main: 主线程设置 ITL 值
Main->>ITL: set("parent-value")
ITL->>Main: 存入 inheritableThreadLocals
Note over Main,Child: 创建子线程
Main->>Thread: new Thread(runnable)
Thread->>Thread: init(..., inheritThreadLocals=true)
Note over Thread: init() 方法执行
Thread->>Main: parent = currentThread()
Thread->>Main: 检查 parent.inheritableThreadLocals
alt parent.inheritableThreadLocals != null
Thread->>Thread: createInheritedMap(parent.inheritableThreadLocals)
Note over Thread: 遍历父线程的 Map<br/>对每个 Entry 调用 childValue()<br/>创建子线程的 Map
Thread->>Child: this.inheritableThreadLocals = 新 Map
end
Note over Child: 子线程启动后
Child->>ITL: get()
ITL->>Child: 从 inheritableThreadLocals 获取
Child-->>Child: 返回 "parent-value"
Thread 为 InheritableThreadLocal 的专门改造
问题:Thread 类是否为 InheritableThreadLocal 专门改造过?
答案:是的,Thread 类进行了以下专门改造:
- 新增字段:
inheritableThreadLocals字段专门用于存储可继承的 ThreadLocal 值 - init() 方法增强:添加了
inheritThreadLocals参数和复制逻辑 - createInheritedMap() 方法:ThreadLocal 类中专门提供了创建继承 Map 的静态方法
1 | |
为什么需要两个 Map?不能合并吗?
问题:为什么 Thread 要有 threadLocals 和 inheritableThreadLocals 两个 Map,不能合并成一个吗?
答案:不能合并,原因如下:
graph TB
subgraph "如果只有一个 Map 的问题"
direction TB
subgraph "父线程"
P_TL1["ThreadLocal A = 敏感数据"]
P_TL2["ThreadLocal B = 普通数据"]
P_ITL["InheritableThreadLocal C = 需要传递的数据"]
style P_TL1 fill:#ffcdd2
style P_TL2 fill:#fff9c4
style P_ITL fill:#c8e6c9
end
arrow["创建子线程时<br/>如果只有一个 Map<br/>要么全部复制,要么全不复制"]
style arrow fill:#ffebee
subgraph "子线程"
C_TL1["ThreadLocal A = 敏感数据 (不应该继承)"]
C_TL2["ThreadLocal B = 普通数据 (不应该继承)"]
C_ITL["InheritableThreadLocal C = 需要传递的数据 (应该继承)"]
style C_TL1 fill:#ffcdd2
style C_TL2 fill:#fff9c4
style C_ITL fill:#c8e6c9
end
end
不能合并的四个核心原因:
| 原因 | 说明 |
|---|---|
| 1. 语义隔离 | 普通 ThreadLocal 的设计初衷是线程隔离,不应该被子线程看到;InheritableThreadLocal 的设计初衷是父子传递。两者语义完全相反 |
| 2. 安全性 | 如果合并,敏感的 ThreadLocal 值(如数据库连接、事务状态)会意外被子线程继承,造成安全隐患 |
| 3. 性能优化 | 分开存储后,创建子线程时只需复制 inheritableThreadLocals,而不是全部 ThreadLocal 值,减少开销 |
| 4. 选择性继承 | 开发者可以明确选择哪些变量需要继承(使用 InheritableThreadLocal),哪些不需要(使用普通 ThreadLocal) |
继承流程对比:
1 | |
InheritableThreadLocal 的浅拷贝问题
InheritableThreadLocal 在继承时存在浅拷贝问题。childValue() 方法默认直接返回父线程的值引用,而不是深拷贝:
1 | |
问题演示:
1 | |
解决方案:重写 childValue() 方法实现深拷贝:
1 | |
InheritableThreadLocal 的局限性
InheritableThreadLocal 的局限性:它只在创建子线程时复制父线程的值。如果使用线程池,线程是复用的,不会每次都创建新线程,因此 InheritableThreadLocal 在线程池场景下无法正确传递上下文。
这就是为什么需要 TransmittableThreadLocal——InheritableThreadLocal 对线程池极不友好,无法满足现代应用中大量使用线程池的场景。
1 | |
原因:线程池中的线程在第一次执行任务时就已经创建完成,此时继承了 request-1。后续任务复用这个线程时,不会再触发 Thread.init() 中的继承逻辑。
TransmittableThreadLocal:线程池场景的解决方案
阿里巴巴开源的 TransmittableThreadLocal (TTL) 解决了线程池场景下的上下文传递问题。
核心挑战:不能修改 Thread 类
InheritableThreadLocal 之所以能实现父子线程传递,是因为 JDK 对 Thread 类进行了专门改造——添加了 inheritableThreadLocals 字段和 init() 方法中的复制逻辑。
但对于第三方库(如 TTL),无法修改 JDK 的 Thread 类。那么如何在不修改 Thread 的情况下,实现线程池场景的上下文传递呢?
TTL 的巧妙解决方案:Capture-Replay-Restore
TTL 采用了一种完全不同的思路——在任务层面而非线程层面解决问题:
sequenceDiagram
participant Main as 主线程
participant TTL as TransmittableThreadLocal
participant Task as TtlRunnable
participant Pool as 线程池
participant Worker as 工作线程
Note over Main: 阶段1:设置上下文
Main->>TTL: context.set("request-123")
TTL->>Main: 存入 inheritableThreadLocals
Note over Main,Task: 阶段2:提交任务时捕获(Capture)
Main->>Task: TtlRunnable.get(runnable)
Task->>Task: capture() 捕获所有 TTL 值
Note over Task: 创建快照:{context: "request-123"}
Main->>Pool: executor.submit(ttlRunnable)
Note over Worker: 阶段3:执行前重放(Replay)
Pool->>Worker: 分配任务给工作线程
Worker->>Task: run()
Task->>Task: backup = replay(captured)
Note over Task: 1. 备份工作线程当前的 TTL 值<br/>2. 将快照中的值设置到工作线程<br/>3. 清理不在快照中的 TTL 变量
Note over Worker: 阶段4:执行任务
Task->>Worker: runnable.run()
Worker->>TTL: context.get()
TTL-->>Worker: 返回 "request-123" (正确)
Note over Worker: 阶段5:执行后恢复(Restore)
Task->>Task: restore(backup)
Note over Task: 恢复工作线程原来的 TTL 值<br/>避免影响后续任务
核心实现原理
1. holder 注册机制
TTL 的关键创新是引入了一个全局注册表,记录所有 TTL 实例:
1 | |
设计精妙之处:
- 使用
WeakHashMap避免内存泄漏 holder本身是InheritableThreadLocal,确保子线程能继承注册表- 每次
set()时自动注册,capture()时遍历注册表获取所有 TTL 值
2. 拦截线程池的 execute 方法
TTL 解决线程池传递问题的核心是拦截线程池的 execute() 方法。无论是 TtlExecutors 包装还是 Java Agent,本质上都是在任务提交时进行拦截:
1 | |
Java Agent 的字节码增强:
1 | |
3. Worker 线程的 ThreadLocalMap Store/Restore
TTL 最关键的设计是处理工作线程原有 ThreadLocalMap 的保存和恢复。这是防止数据污染的核心机制:
sequenceDiagram
participant Task as TtlRunnable
participant Worker as 工作线程
participant Map as ThreadLocalMap
Note over Worker: 工作线程可能有自己的 TTL 值<br/>(来自之前执行的其他任务)
Task->>Worker: run() 开始执行
rect rgb(255, 245, 238)
Note over Task,Map: Store 阶段
Task->>Map: backup = 备份当前 ThreadLocalMap 中的 TTL 值
Task->>Map: 清理不在 captured 中的 TTL 变量
Task->>Map: 设置 captured 中的值到 ThreadLocalMap
end
Task->>Task: 执行原始任务 runnable.run()
rect rgb(232, 245, 233)
Note over Task,Map: Restore 阶段
Task->>Map: 清理当前 ThreadLocalMap 中的 TTL 变量
Task->>Map: 从 backup 恢复原来的 TTL 值
end
Note over Worker: 工作线程恢复到执行任务前的状态<br/>不影响后续任务
Store/Restore 的源码实现:
1 | |
为什么需要 Store/Restore?
| 场景 | 不做 Restore 的问题 |
|---|---|
| 任务 A 设置了 TTL 值 | 任务 B 复用同一个工作线程时,会读到任务 A 的值 |
| 工作线程有自己的 TTL 值 | 任务执行后,工作线程原有的值被覆盖,影响后续逻辑 |
| 任务执行中修改了 TTL 值 | 修改会"泄漏"到后续任务,造成数据污染 |
Store/Restore 确保:
- 任务执行时只能看到提交任务时父线程传递的值
- 任务执行完毕后,工作线程恢复到执行任务前的状态
- 任务之间完全隔离,互不影响
4. Transmitter 工具类
1 | |
3. TtlRunnable 包装器
1 | |
为什么需要清理不在快照中的 TTL 变量?
这是 TTL 设计中最精妙的部分。考虑以下场景:
1 | |
清理逻辑确保:任务执行时只能访问提交任务时父线程传递的值,而不是工作线程之前执行其他任务时遗留的值。
三种使用方式对比
| 方式 | 侵入性 | 实现原理 | 适用场景 |
|---|---|---|---|
| TtlRunnable 包装 | 高 | 手动包装每个任务 | 少量任务需要传递上下文 |
| TtlExecutors 包装 | 中 | 装饰器模式包装线程池 | 特定线程池需要传递上下文 |
| Java Agent | 无 | 字节码增强,自动包装 | 全局透明传递,推荐生产使用 |
方式一:修饰 Runnable/Callable
1 | |
方式二:修饰线程池
1 | |
方式三:Java Agent 方式(推荐)
通过 Java Agent 在类加载时自动增强线程池,无需修改业务代码:
1 | |
Java Agent 的实现原理是在类加载时修改 ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool 等类的字节码,自动将提交的 Runnable/Callable 包装为 TTL 版本。
TTL 核心数据结构图
graph TB
subgraph "TTL 核心数据结构"
direction TB
subgraph Thread1["主线程 (提交任务)"]
T1_TLM["threadLocals<br/>(ThreadLocalMap)"]
T1_ITL["inheritableThreadLocals<br/>(ThreadLocalMap)"]
T1_HOLDER["holder.get()<br/>(WeakHashMap)"]
T1_TTL1["TTL1 → value1"]
T1_TTL2["TTL2 → value2"]
T1_TLM --> T1_TTL1
T1_TLM --> T1_TTL2
T1_HOLDER --> |"注册"| T1_TTL1
T1_HOLDER --> |"注册"| T1_TTL2
end
subgraph Snapshot["捕获的快照 (Snapshot)"]
S_MAP["WeakHashMap<br/>TTL → Value 副本"]
S_TTL1["TTL1 → value1_copy"]
S_TTL2["TTL2 → value2_copy"]
S_MAP --> S_TTL1
S_MAP --> S_TTL2
end
subgraph Thread2["工作线程 (执行任务)"]
T2_TLM["threadLocals<br/>(ThreadLocalMap)"]
T2_BACKUP["backup<br/>(原有值备份)"]
T2_OLD["TTL3 → old_value"]
T2_TLM --> T2_OLD
end
T1_HOLDER -->|"capture()"| Snapshot
Snapshot -->|"replay()"| T2_TLM
T2_OLD -->|"备份到"| T2_BACKUP
end
style T1_HOLDER fill:#e3f2fd
style Snapshot fill:#fff3e0
style T2_BACKUP fill:#e8f5e9
TTL 完整生命周期图
flowchart TB
subgraph Phase1["阶段1: 主线程设置值"]
P1_SET["context.set('request-123')"]
P1_TLM["存入 threadLocals"]
P1_REG["注册到 holder"]
P1_SET --> P1_TLM
P1_SET --> P1_REG
end
subgraph Phase2["阶段2: 提交任务时捕获"]
P2_WRAP["TtlRunnable.get(runnable)"]
P2_CAP["Transmitter.capture()"]
P2_ITER["遍历 holder 中所有 TTL"]
P2_COPY["复制每个 TTL 的值"]
P2_SNAP["创建 Snapshot"]
P2_WRAP --> P2_CAP
P2_CAP --> P2_ITER
P2_ITER --> P2_COPY
P2_COPY --> P2_SNAP
end
subgraph Phase3["阶段3: 工作线程执行前"]
P3_RUN["TtlRunnable.run()"]
P3_REPLAY["Transmitter.replay(captured)"]
P3_BACKUP["备份工作线程当前 TTL 值"]
P3_CLEAN["清理不在快照中的 TTL"]
P3_APPLY["应用快照中的值"]
P3_RUN --> P3_REPLAY
P3_REPLAY --> P3_BACKUP
P3_BACKUP --> P3_CLEAN
P3_CLEAN --> P3_APPLY
end
subgraph Phase4["阶段4: 执行业务逻辑"]
P4_BIZ["runnable.run()"]
P4_GET["context.get()"]
P4_VAL["返回 'request-123' (正确)"]
P4_BIZ --> P4_GET
P4_GET --> P4_VAL
end
subgraph Phase5["阶段5: 执行后恢复"]
P5_RESTORE["Transmitter.restore(backup)"]
P5_CLEAR["清理当前 TTL 值"]
P5_RECOVER["从 backup 恢复原有值"]
P5_DONE["工作线程状态恢复"]
P5_RESTORE --> P5_CLEAR
P5_CLEAR --> P5_RECOVER
P5_RECOVER --> P5_DONE
end
Phase1 --> Phase2
Phase2 --> Phase3
Phase3 --> Phase4
Phase4 --> Phase5
style Phase1 fill:#e8f5e9
style Phase2 fill:#fff3e0
style Phase3 fill:#e3f2fd
style Phase4 fill:#f3e5f5
style Phase5 fill:#ffebee
TTL 多任务隔离机制图
sequenceDiagram
participant Main as 主线程
participant Pool as 线程池
participant W1 as Worker-1
Note over Main,W1: 场景:两个任务复用同一个工作线程
rect rgb(232, 245, 233)
Note over Main: 任务1:设置 context = "task1"
Main->>Main: context.set("task1")
Main->>Pool: submit(TtlRunnable(task1))
Note over Pool: 快照1: {context: "task1"}
end
rect rgb(255, 243, 224)
Pool->>W1: 分配任务1
W1->>W1: backup = replay(快照1)
Note over W1: backup = {} (工作线程原本为空)
W1->>W1: 设置 context = "task1"
W1->>W1: 执行 task1
W1->>W1: context.set("modified") 任务中修改
W1->>W1: restore(backup)
Note over W1: 清理 context,恢复为空
end
rect rgb(227, 242, 253)
Note over Main: 任务2:设置 context = "task2"
Main->>Main: context.set("task2")
Main->>Pool: submit(TtlRunnable(task2))
Note over Pool: 快照2: {context: "task2"}
end
rect rgb(243, 229, 245)
Pool->>W1: 分配任务2 (复用同一线程)
W1->>W1: backup = replay(快照2)
Note over W1: backup = {} (已被恢复为空)
W1->>W1: 设置 context = "task2"
W1->>W1: 执行 task2
W1->>W1: context.get() 返回 "task2" (正确)
Note over W1: 不会读到 "modified"!
W1->>W1: restore(backup)
end
TTL 三种使用方式对比图
flowchart LR
subgraph Way1["方式1: 包装 Runnable"]
W1_CODE["TtlRunnable.get(runnable)"]
W1_PROS["优点: 精确控制"]
W1_CONS["缺点: 侵入性高"]
W1_CODE --> W1_PROS
W1_CODE --> W1_CONS
end
subgraph Way2["方式2: 包装线程池"]
W2_CODE["TtlExecutors.getTtlExecutorService(executor)"]
W2_PROS["优点: 一次包装"]
W2_CONS["缺点: 需修改代码"]
W2_CODE --> W2_PROS
W2_CODE --> W2_CONS
end
subgraph Way3["方式3: Java Agent"]
W3_CODE["-javaagent:ttl.jar"]
W3_PROS["优点: 零侵入"]
W3_CONS["缺点: 需要 JVM 参数"]
W3_CODE --> W3_PROS
W3_CODE --> W3_CONS
end
Way1 -->|"更便捷"| Way2
Way2 -->|"更透明"| Way3
style Way1 fill:#ffebee
style Way2 fill:#fff3e0
style Way3 fill:#e8f5e9
holder 注册机制详解图
graph TB
subgraph TTL_Instance["TTL 实例"]
TTL1["TTL1: traceId"]
TTL2["TTL2: userId"]
TTL3["TTL3: tenantId"]
end
subgraph SetOps["set 操作"]
SET1["ttl1.set(trace-001)"]
SET2["ttl2.set(user-123)"]
SET3["ttl3.set(tenant-A)"]
end
subgraph HolderMap["holder WeakHashMap"]
REG1["TTL1 -> null"]
REG2["TTL2 -> null"]
REG3["TTL3 -> null"]
end
subgraph CaptureFlow["capture 遍历"]
CAP1["遍历 holder.keySet"]
CAP2["对每个TTL调用get"]
CAP3["生成快照Map"]
CAP1 --> CAP2
CAP2 --> CAP3
end
SET1 --> TTL1
SET1 --> REG1
SET2 --> TTL2
SET2 --> REG2
SET3 --> TTL3
SET3 --> REG3
HolderMap --> CAP1
style HolderMap fill:#e3f2fd
style CaptureFlow fill:#fff3e0
InheritableThreadLocal vs TransmittableThreadLocal
graph TB
subgraph "InheritableThreadLocal(JDK 内置)"
direction TB
ITL_HOW["实现方式:修改 Thread 类"]
ITL_WHEN["传递时机:创建子线程时"]
ITL_LIMIT["局限性:线程池场景失效"]
style ITL_LIMIT fill:#ffcdd2
end
subgraph "TransmittableThreadLocal(阿里开源)"
direction TB
TTL_HOW["实现方式:包装任务/线程池"]
TTL_WHEN["传递时机:提交任务时"]
TTL_ADVANTAGE["优势:完美支持线程池"]
style TTL_ADVANTAGE fill:#c8e6c9
end
subgraph "设计对比"
ITL_DESIGN["Thread 持有 Map<br/>在 init() 中复制"]
TTL_DESIGN["holder 注册所有 TTL<br/>capture/replay/restore"]
end
适用场景
- 分布式追踪:TraceId、SpanId 的跨线程传递
- 日志上下文:MDC(Mapped Diagnostic Context)的传递
- 用户上下文:用户身份信息、租户信息的传递
- 事务上下文:分布式事务的上下文传递
ThreadLocal 核心源码深度解析
ThreadLocal.set() 源码详解
1 | |
ThreadLocal.get() 源码详解
1 | |
ThreadLocalMap.set() 源码详解
1 | |
ThreadLocalMap.getEntry() 源码详解
1 | |
expungeStaleEntry() 核心清理逻辑
1 | |
基于 ThreadLocal 的设计模式框架实例
实例1:Spring 的 RequestContextHolder
Spring 框架使用 ThreadLocal 实现请求上下文的线程隔离:
1 | |
使用示例:
1 | |
实例2:SLF4J 的 MDC(Mapped Diagnostic Context)
SLF4J 的 MDC 使用 ThreadLocal 实现日志上下文的线程隔离:
1 | |
使用示例:
1 | |
实例3:MyBatis 的 SqlSession 管理
MyBatis-Spring 使用 ThreadLocal 管理 SqlSession 的生命周期:
1 | |
实例4:自定义租户上下文(多租户架构)
1 | |
实例5:安全上下文(类似 Spring Security)
1 | |
设计模式总结
graph TB
subgraph "基于 ThreadLocal 的设计模式"
direction TB
subgraph Pattern1["Holder 模式"]
H1["静态 ThreadLocal 变量"]
H2["静态 get/set/remove 方法"]
H3["示例: RequestContextHolder"]
end
subgraph Pattern2["上下文对象模式"]
C1["封装多个相关属性"]
C2["提供便捷访问方法"]
C3["示例: TenantContext"]
end
subgraph Pattern3["策略模式"]
S1["多种存储策略"]
S2["运行时可切换"]
S3["示例: SecurityContextHolder"]
end
subgraph Pattern4["资源绑定模式"]
R1["绑定资源到线程"]
R2["事务内复用"]
R3["示例: TransactionSynchronizationManager"]
end
subgraph Pattern5["诊断上下文模式"]
D1["键值对存储"]
D2["日志自动附加"]
D3["示例: MDC"]
end
end
style Pattern1 fill:#e3f2fd
style Pattern2 fill:#e8f5e9
style Pattern3 fill:#fff3e0
style Pattern4 fill:#f3e5f5
style Pattern5 fill:#ffebee
最佳实践
如何正确使用 ThreadLocal
ThreadLocal 最好的用法是做一个 request scope 的缓存——在请求开始时设置,请求结束时清理。在线程里长期复用 ThreadLocal 其实极度危险。
正确的使用模式:
1 | |
最佳实践清单:
- 手动清理:使用完 ThreadLocal 后立即调用
remove(),这是最可靠的方式 - 声明为 static:ThreadLocal 变量应该声明为 static,避免每个对象实例都创建新的 ThreadLocal
- 避免静态变量泄漏:谨慎管理 ThreadLocal 静态变量的生命周期
- 线程池场景特别注意:在使用线程池时,线程不会被销毁,必须手动清理
- 使用 try-finally:确保在 finally 块中调用 remove(),即使发生异常也能清理
WeakHashMap 与 ThreadLocalMap 的设计对比
WeakHashMap 和 ThreadLocalMap 有相似的设计理念——利用弱引用实现自动清理。
WeakHashMap 利用下一次操作来触发 clear,好像有一个后台线程来维护 Map 一样。这种"惰性清理"的设计模式在 ThreadLocalMap 中也有体现:只有在 get/set/remove 操作时才会触发 Stale Entry 的清理。
这种设计的优点是避免了额外的清理线程开销,缺点是如果长时间没有操作,过期数据不会被及时清理。
总结
ThreadLocal 是 Java 并发编程中实现线程封闭的核心工具,其设计体现了几个重要的工程智慧:
- 反转持有关系:让 Thread 持有 Map,而不是让 Map 持有 Thread,从根本上避免了线程无法回收的问题
- 弱引用 + 惰性清理:通过弱引用 key 和惰性清理机制,在不影响性能的前提下尽可能避免内存泄漏
- 开放地址法:针对 ThreadLocal 的使用特点(Entry 数量少、需要顺便清理 Stale Entry),选择了更合适的哈希冲突解决方案
在实际使用中,要牢记:
- ThreadLocal 应该声明为 static
- 使用完毕后必须调用 remove()
- 线程池场景下考虑使用 TransmittableThreadLocal





