@@ -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+ Thread → ThreadLocalMap → 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
205225JDK 默认没有支持线程池场景下 ` 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
2273731 . ** 压测流量标记** : 在压测场景中,使用 ` ThreadLocal ` 存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。
2283742 . ** 上下文传递** :在分布式系统中,传递链路追踪信息(如 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### 什么是线程池?
0 commit comments