Skip to content

Commit 2c4d22b

Browse files
committed
update:完善如何跨线程传递 ThreadLocal 的值
1 parent 97a9e96 commit 2c4d22b

File tree

2 files changed

+170
-13
lines changed

2 files changed

+170
-13
lines changed

docs/java/concurrent/java-concurrent-questions-03.md

Lines changed: 167 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,26 @@ static class Entry extends WeakReference<ThreadLocal<?>> {
162162

163163
### ⭐️如何跨线程传递 ThreadLocal 的值?
164164

165-
由于 `ThreadLocal` 的变量值存放在 `Thread` 里,而父子线程属于不同的 `Thread` 的。因此在异步场景下,父子线程的 `ThreadLocal` 值无法进行传递。
165+
**为什么 ThreadLocal 在异步场景下会失效?**
166166

167-
如果想要在异步场景下传递 `ThreadLocal` 值,有两种解决方案
167+
`ThreadLocal` 的值不在 `ThreadLocal` 对象中,而是存储在 `Thread`
168168

169-
- `InheritableThreadLocal``InheritableThreadLocal` 是 JDK1.2 提供的工具,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。
170-
- `TransmittableThreadLocal``TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:<https://github.com/alibaba/transmittable-thread-local>
169+
```java
170+
ThreadThreadLocalMap → Entry(ThreadLocal, value)
171+
```
172+
173+
`ThreadLocal` 数据结构如下图所示:
174+
175+
![ThreadLocal 数据结构](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadlocal-data-structure.png)
176+
177+
异步执行往往意味着任务会从当前线程切换到另一个线程(例如线程池中的工作线程)执行。由于不同线程各自维护独立的 `ThreadLocalMap`,默认情况下 `ThreadLocal` 的上下文无法在异步执行中自动传递。
178+
179+
**如何跨线程传递 ThreadLocal 的值?**
180+
181+
为了解决这个问题,业界有两套主流的解决方案,一套是 JDK 原生的,另一套是阿里巴巴开源的。
182+
183+
1. `InheritableThreadLocal` :JDK1.2 提供的一个类,继承自 `ThreadLocal` 。使用 `InheritableThreadLocal` 时,会在创建子线程时,令子线程继承父线程中的 `ThreadLocal` 值,但是无法支持线程池场景下的 `ThreadLocal` 值传递。
184+
2. `TransmittableThreadLocal``TransmittableThreadLocal` (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了`InheritableThreadLocal`类,可以在线程池的场景下支持 `ThreadLocal` 值传递。项目地址:<https://github.com/alibaba/transmittable-thread-local>
171185

172186
#### InheritableThreadLocal 原理
173187

@@ -200,33 +214,176 @@ private void init(/* ... */) {
200214
}
201215
```
202216

217+
**`InheritableThreadLocal` 的方案有什么问题?**
218+
219+
这个方案的缺陷在于它的**一次性**,也就是它只在线程创建时发生一次复制。然而,现在的开发中,我们会大量使用线程池,但线程池里的线程是被复用的。
220+
221+
想象一下,任务A在线程1中执行,把它的 `ThreadLocal` 值传给了线程池里的子线程2。任务A结束后,线程1去休息了。接着,任务B来了,它在线程3中执行,线程池又复用了刚才那个子线程2来执行任务B的一部分。此时,子线程2的`ThreadLocal`里还残留着任务A传给它的脏数据,而任务B(在线程3里)的上下文却完全没有传递过来。这就导致了数据污染和上下文丢失。
222+
203223
#### TransmittableThreadLocal 原理
204224

205225
JDK 默认没有支持线程池场景下 `ThreadLocal` 值传递的功能,因此阿里巴巴开源了一套工具 `TransmittableThreadLocal` 来实现该功能。
206226

207-
阿里巴巴无法改动 JDK 的源码,因此他内部通过 **装饰器模式** 在原有的功能上做增强,以此来实现线程池场景下的 `ThreadLocal` 值传递。
227+
由于阿里巴巴无法改动 JDK 源码,TTL 巧妙地利用了**装饰器模式**对任务(`Runnable`/`Callable`)或线程池(`Executor`)进行增强,将上下文的传递时机从“线程创建时”延迟到了“任务提交与执行时”。
228+
229+
TTL 的核心逻辑可以概括为三个阶段(CRR):
230+
231+
- **Capture(捕获)**:在提交任务(如调用 `execute`)的一瞬间,`TtlRunnable` 会调用 `TransmittableThreadLocal.Transmitter.capture()`。它通过内部维护的 `holder` 集合,抓取当前父线程中所有活跃的 TTL 变量并存入快照。
232+
- **Replay(回放)**:在线程池的工作线程执行 `run()` 方法前,调用 `replay()`。它将快照中的值 `set` 到当前工作线程中,并备份该线程原有的旧值。
233+
- **Restore(恢复)**:任务执行结束后,调用 `restore()`。它根据备份将工作线程恢复到执行前的状态,防止上下文污染或内存泄漏。
234+
235+
这张图是 TTL 官方提供的 CRR 整个过程的时序图:
236+
237+
![TTL 官方提供的 CRR 整个过程的时序图](https://oss.javaguide.cn/github/javaguide/java/concurrent/ttl-crr-timing-diagram.png)
238+
239+
不太好理解吧?可以看下我绘制的这张 CRR 时序图,更清晰直观一些:
240+
241+
```mermaid
242+
sequenceDiagram
243+
participant P as 父线程(Submitter)
244+
participant W as TTL 包装器(TtlRunnable / Agent)
245+
participant C as 线程池工作线程(Worker)
246+
247+
Note over P: 1. set context = "A"
248+
P->>W: 2. 提交任务(Capture)
249+
Note right of W: 捕获父线程中所有活跃的 TTL 变量快照
250+
251+
W->>C: 3. 执行任务 run()
252+
Note over C: 4. Replay
253+
Note right of C: 备份工作线程原有 TTL 值<br/>并设置 Capture 得到的值
254+
255+
Note over C: 5. 业务逻辑执行<br/>get context = "A"
256+
257+
Note over C: 6. Restore
258+
Note right of C: 恢复工作线程原有 TTL 值<br/>防止上下文污染
259+
260+
C-->>P: 7. 任务执行结束
261+
262+
```
263+
264+
也就是说,TTL 的本质是在任务提交时 Capture 上下文,在任务执行前 Replay 上下文,在任务结束后 Restore 线程状态,从而安全地支持线程池中的 `ThreadLocal` 传递。
265+
266+
TTL 提供了两种主要的接入方式,可根据侵入性要求和改造成本进行选择。
267+
268+
**1. 显式包装(手动接入)**
269+
270+
使用 `TtlRunnable.get(Runnable)``TtlCallable.get(Callable)` 对任务进行包装,使用 `TtlExecutors.getTtlExecutor(Executor)``getTtlExecutorService(...)` 对线程池进行包装。这种接入方式清晰可控,但需要业务代码配合,存在一定侵入性。
271+
272+
下面这段代码展示了 TTL 通过 CRR,在支持线程池复用和拒绝策略的前提下,安全地传递并隔离 `ThreadLocal` 上下文。
208273

209-
TTL 改造的地方有两处:
274+
```java
275+
public class TtlContextHolder {
276+
private static final Logger log = LoggerFactory.getLogger(TtlContextHolder.class);
277+
278+
// 1. 使用 static final 确保 TTL 实例不被重复创建,防止内存泄漏
279+
// 重写 copy 方法(可选):如果是引用类型,建议实现深拷贝
280+
private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<String>() {
281+
@Override
282+
public String copy(String parentValue) {
283+
// 默认是直接返回引用,如果是可变对象(如 Map),请在这里 new 新对象
284+
return parentValue;
285+
}
286+
};
287+
288+
// 2. 线程池初始化:确保只被 TtlExecutors 包装一次
289+
private static final ExecutorService TTL_EXECUTOR_SERVICE;
290+
291+
static {
292+
ExecutorService rawExecutor = new ThreadPoolExecutor(
293+
2, 4, 60L, TimeUnit.SECONDS,
294+
new LinkedBlockingQueue<>(1000), (Runnable r) -> new Thread(r, "ttl-worker-" + r.hashCode()),
295+
new ThreadPoolExecutor.CallerRunsPolicy() // 关键:TTL 完美支持此拒绝策略
296+
);
297+
// 包装原始线程池
298+
TTL_EXECUTOR_SERVICE = TtlExecutors.getTtlExecutorService(rawExecutor);
299+
}
300+
301+
public static void main(String[] args) throws Exception {
302+
try {
303+
// 3. 在父线程中设置上下文
304+
CONTEXT.set("value-set-in-parent");
305+
log.info("父线程上下文: {}", CONTEXT.get());
306+
307+
// 4. 使用 Lambda 简化任务提交
308+
TTL_EXECUTOR_SERVICE.submit(() -> {
309+
log.info("异步任务(Runnable)读取上下文: {}", CONTEXT.get());
310+
// 模拟业务逻辑
311+
// 注意:子线程修改是否影响父线程,取决于 copy() 是否做了深拷贝
312+
CONTEXT.set("value-modified-in-child");
313+
});
314+
315+
Future<String> future = TTL_EXECUTOR_SERVICE.submit(() -> {
316+
log.info("异步任务(Callable)读取上下文: {}", CONTEXT.get());
317+
return "Success";
318+
});
210319

211-
- 实现自定义的 `Thread` ,在 `run()` 方法内部做 `ThreadLocal` 变量的赋值操作。
320+
future.get();
212321

213-
- 基于 **线程池** 进行装饰,在 `execute()` 方法中,不提交 JDK 内部的 `Thread` ,而是提交自定义的 `Thread`
322+
// 5. 验证父线程上下文是否被污染
323+
log.info("父线程最终上下文: {}", CONTEXT.get());
214324

215-
如果想要查看相关源码,可以引入 Maven 依赖进行下载。
325+
} finally {
326+
// 6. 清理当前线程(父线程)的上下文,子线程的上下文由 TTL 的 Restore 机制自动恢复
327+
CONTEXT.remove();
328+
}
329+
}
330+
}
331+
```
332+
333+
输出:
334+
335+
```ba
336+
09:06:31.438 INFO [main] TtlContextHolder - 父线程上下文: value-set-in-parent
337+
09:06:31.452 INFO [ttl-worker-1663166483] TtlContextHolder - 异步任务(Runnable)读取上下文: value-set-in-parent
338+
09:06:31.453 INFO [ttl-worker-841283083] TtlContextHolder - 异步任务(Callable)读取上下文: value-set-in-parent
339+
09:06:31.453 INFO [main] TtlContextHolder - 父线程最终上下文: value-set-in-parent
340+
```
341+
342+
如果你想要测试这段代码,记得引入 TTL 的 Maven 依赖;
216343

217344
```XML
218345
<dependency>
219346
<groupId>com.alibaba</groupId>
220347
<artifactId>transmittable-thread-local</artifactId>
221-
<version>2.12.0</version>
348+
<version>2.14.4</version>
222349
</dependency>
223350
```
224351

352+
**2. 无侵入接入(Java Agent)**
353+
354+
通过 Java Agent 在类加载阶段对线程池相关类进行 字节码增强,自动织入 TTL 的上下文传递逻辑,实现业务代码零改造的上下文透传。这种方式业务代码无需感知 TTL 的存在,但实现复杂度相对较高。
355+
356+
TTL Agent 默认修饰了以下 JDK 执行器组件:
357+
358+
1. **标准线程池**`java.util.concurrent.ThreadPoolExecutor``java.util.concurrent.ScheduledThreadPoolExecutor`
359+
2. **ForkJoin 体系**`java.util.concurrent.ForkJoinTask`(从而透明支持了 `CompletableFuture` 和 Java 8 并行流 `Stream`)。
360+
3. **遗留组件**`java.util.TimerTask`(自 v2.7.0 起支持,v2.11.2 起默认开启)。
361+
362+
在 Java 启动参数中加入 `-javaagent` 配置:
363+
364+
```bash
365+
# 基础配置
366+
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
367+
-cp classes \
368+
com.your.app.Main
369+
```
370+
225371
#### 应用场景
226372

227373
1. **压测流量标记**: 在压测场景中,使用 `ThreadLocal` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。
228374
2. **上下文传递**:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。
229375

376+
#### 总结
377+
378+
`ThreadLocal` 的值默认是无法跨线程传递的,因为它的值是存在**每个 `Thread` 对象自己**`ThreadLocalMap` 里的,父子线程是两个不同的对象。
379+
380+
为了解决这个问题,主要有两种方案:
381+
382+
1. **JDK的 InheritableThreadLocal**:它会在**创建子线程**的时候,把父线程的值**复制**一份给子线程。但它的问题是,在**线程池**场景下会失效。因为线程池会**复用**线程,这会导致线程拿到的可能是上一个任务传下来的**脏数据**
383+
2. **阿里的 TransmittableThreadLocal (TTL)**:这是我们项目里用的方案,它专门解决线程池的问题。它的原理是,在**提交任务**到线程池时,它会把父线程的 `ThreadLocal`**捕获**下来,和任务**绑定**在一起。等线程池里的某个线程要执行这个任务时,它再把捕获的值**设置**到这个线程上,任务执行完再**清理**掉。
384+
385+
简单说,**InheritableThreadLocal是跟线程绑定的,只在创建时有效;而TTL是跟任务绑定的,完美支持线程池。**
386+
230387
## 线程池
231388

232389
### 什么是线程池?

docs/zhuanlan/interview-guide.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,9 @@ PostgreSQL 最大的优势,也是它在 AI 时代甩开对手的“王牌”
293293

294294
选择 Redis Stream 的理由:
295295

296-
- 复用现有组件:Redis 已用于会话缓存,无需引入新中间件
297-
- 功能满足需求:支持消费者组、消息确认(ACK)、持久化
298-
- 运维简单:对于中小型项目,Redis Stream 完全够用
296+
- 复用现有组件:Redis 已用于会话缓存,无需引入新中间件
297+
- 功能满足需求:支持消费者组、消息确认(ACK)、持久化
298+
- 运维简单:对于中小型项目,Redis Stream 完全够用
299299

300300
### 构建工具为什么选择 Gradle?
301301

0 commit comments

Comments
 (0)