TransmittableThreadLocal 四千字超详细讲解

TransmittableThreadLocal

ThreadLocal 前情提要

ThreadLocal (后续简称TL) 为本地线程遍历,用于实现线程之间的变量隔离

实现原理

Thead 线程类 持有ThreadMap 集合,key为ThreadLocal 引用,value 为ThreadLocal 调用Set 时设置的Value,通过ThreadLocal 的引用 就可以在当前线程 Thread 类 中的ThreadMap 拿到 对应的Value

源码细节

翻看Thead 类 中你会发现 ThreadLocal.ThreadLocalMap threadLocals = null; 同时在Thread.init() 方法中并没有对threadLocals 进行赋值,这是jdk惯用的懒加载,当你的线程第一次调用了 ThreadLoacl.set() 方法时会触发 ThreadLocal.ThreadLocalMap 的初始化

 

java

体验AI代码助手

代码解读

复制代码

void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

具体初始化方法,没有特别需要注意的

 

java

体验AI代码助手

代码解读

复制代码

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }

因为本身集合的维度就是线程维度的,所以不需要考虑并发安全

InheritableThreadLocal 提要

在讲解 TransmittableThreadLocal (后续简称TTL) 还需要了解这个InheritableThreadLocal (后续简称ITL)类 因为TTL很多的功能实际是继承ITL获得的

InheritableThreadLocal 是JDK 为了解决Tl 在 父子线程的传值问题。

实现原理

重写了TL的 getMap 与 createMap 方法,使其调用线程的 inheritableThreadLocals

 

java

体验AI代码助手

代码解读

复制代码

ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }

而 inheritableThreadLocals 则是在 Thread.init() 方法时从主线程进行赋值构造

 

java

体验AI代码助手

代码解读

复制代码

if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

createInheritedMap 方法

 

java

体验AI代码助手

代码解读

复制代码

private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }

需要注意的是ITL并不能然你选择使用深拷贝而只能使用浅拷贝

**测试代码 **

 

java

体验AI代码助手

代码解读

复制代码

static class Student{ String name; int age; public Student() { this.name = "zhangsan"; this.age = 18; } } public static void main(String[] args) { InheritableThreadLocal<Student> threadLocal = new InheritableThreadLocal<>(); threadLocal.set(new Student()); new Thread(()->{ // 父子线程TL值传递 Student student = threadLocal.get(); System.out.println(student); // 修改引用对象值 student.name = "lisi"; student.age = 20; }).start(); try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(threadLocal.get()); }

结果

 

ini

体验AI代码助手

代码解读

复制代码

Student{name='zhangsan', age=18} Student{name='lisi', age=20}

总结

ITL 只是简单的实现了TLMap的线程上下文的赋值传递,但距离实际项目中使用还差很多功能,

  • 比如池化线程复用时的TL必须要在每次任务末尾进行情况否则下次任务会出现上次任务的TL值,

  • 部分业务场景需要引用对象应该遵循单向传递(就像 Vue 的父子组件的props传递那样)

  • 如果线程池触发了 CallerRunsPolicy 拒绝策略时由调用线程执行了任务末尾的ITL.remove 则会影响后续逻辑

回到正题 TransmittableThreadLocal

引用 Github 文档 内容:

TransmittableThreadLocal 关注的是 上下文传递流程的规范化。上下文传递到了子线程要做好 *清理*(或更准确地说是要 *恢复* 成之前的上下文),需要业务逻辑去处理好。如果业务逻辑对清理的处理不正确,比如:

  • 如果清理操作漏了:
    • 下一次执行可能是上次的,即『上下文的 *污染*/*串号*』,会导致业务逻辑错误。
    • 『上下文的 *泄漏*』,会导致内存泄漏问题。
  • 如果清理操作做多了,会出现上下文 *丢失*

期望:上下文生命周期的操作从业务逻辑中分离出来。业务逻辑不涉及生命周期,就不会有业务代码如疏忽清理而引发的问题了。整个上下文的传递流程或说生命周期可以规范化成:捕捉、回放和恢复这3个操作,即*CRR(capture/replay/restore)模式*。更多讨论参见 Issue:能在详细讲解一下replay、restore的设计理念吗?#201

简单回顾使用TransmittableThreadLocal

如果使用普通线程池搭配 TransmittableThreadLocal 则需要对任务进行包装成 TtlRunnable

 

java

体验AI代码助手

代码解读

复制代码

// 在主线程中设置TransmittableThreadLocal的值 threadLocal.set("Hello, World!"); // 在线程池中执行任务 注意包装 TtlRunnable! executorService.execute(TtlRunnable.get(() -> { String value = threadLocal.get(); System.out.println("TransmittableThreadLocal value in new thread: " + value); })); // 等待任务执行完成 executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS); }

如果不希望每次执行任务都包装一边则使用包装线程池,后续执行任务时自动包装为TtlRunnable 任务

 

java

体验AI代码助手

代码解读

复制代码

executorService = TtlExecutors.getTtlExecutorService(threadPoolTaskExecutor); // 后续使用executorService执行普通任务 // 在主线程中设置TransmittableThreadLocal的值 threadLocal.set("Hello, World!"); executorService.execute(() -> { String value = threadLocal.get(); System.out.println("TransmittableThreadLocal value in new thread: " + value); }); // 等待任务执行完成 executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS);

下面进入源码解读内容,建议点开源码分析

TtlExecutors 是如何解决TTL值在执行任务时的赋值以及自动清除的?

首先我们需要从使用方法中获取线索,也就是 TtlRunnable.get() 这个对任务类对Run包装了什么?

直接找到 TtlRunnable Run 方法( 这时候执行线程已经是子线程了!)

 

java

体验AI代码助手

代码解读

复制代码

@Override public void run() { // 虽然不知道这个capture具体是什么但是大致能够猜到这个就是从主线程保存的TTL值 final Object captured = capturedRef.get(); // 安全检查,注意这里的CAS操作将capturedRef设置为了Null,所以TtlRunnable只能执行一次,后续执行时会触发 captured == null 抛出异常 if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } // replay (重现)结合最后面的 restore (恢复)看起来像是将执行任务前的TTL环境获取出来用户后续的恢复 final Object backup = replay(captured); try { // 真正的任务执行 runnable.run(); } finally { // 看起来像恢复TTL环境 restore(backup); } }

上述注释是根据其功能以及方法名进行的猜测,但其实也是TTL的实现全流程。上面的问题其实也有答案了

Q: TTl 是如何进行父子线程的TTL值传递 ?

A: 通过快照的获取以及快照加载至当前线程的TL

Q:TTL 是如何做到池化线程在执行任务完毕后自动清除主线程传递的TL值

A:通过restore 方法进行还原

看完了 TTLRunable.run 我们回到 TTLRunable.get 方法,这里存在一些对任务执行的一些配置参数,并且构造了一个TTLRunAble 任务,主线程执行!

get()

 

java

体验AI代码助手

代码解读

复制代码

@Nullable public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) { if (null == runnable) return null; if (runnable instanceof TtlEnhanced) { // avoid redundant decoration, and ensure idempotency if (idempotent) return (TtlRunnable) runnable; else throw new IllegalStateException("Already TtlRunnable!"); } // 源代码的 idempotent 是置为false的 return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun, idempotent ); }

了解两个配置参数 releaseTtlValueReferenceAfterRun 与 idempotent

releaseTtlValueReferenceAfterRun :是否释放关联线程的Tl

以下为GPT给出的解释:

当设置为 true 时,TransmittableThreadLocal 的值在任务执行结束后会被自动清理。

  • 这意味着任务线程在任务结束后不会持有 ThreadLocal 的值,从而避免可能的内存泄漏问题。

  • 适用于线程池中线程会被重复使用的场景,因为清理 ThreadLocal 的值可以避免后续任务意外访问到之前任务的值。

idempotent 参数控制的是:当输入的 Runnable 已经是 TtlRunnable 类型时,是否允许直接返回它而不进行额外的包装

TransmittableThreadLocal 在源码中只有一次使用,就是在TTLRunable.run 时使用,注释虽然写了会自动清理但实际上什么也没有做

TtlRunnable构造方法(主线程执行)
 

java

体验AI代码助手

代码解读

复制代码

private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { // 看起来就是这行代码获取到了主线程的TL环境,重点就是这个capture()方法 this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; }

这里的 capturedRef 不就是run方法里的TL快照吗?所以在构建TTLRunable 时就准备好了相关环境

主线程的 TL 快照生成 capture()
 

java

体验AI代码助手

代码解读

复制代码

public static Object capture() { // 这里的第二个参数是指主线程通过`TtlThreadLocal.registerThreadLocal` 注册的普通TL return new Snapshot(captureTtlValues(), captureThreadLocalValues()); }

Ttl 与 普通Tl 快照生成
 

java

体验AI代码助手

代码解读

复制代码

private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() { HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value; } private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() { final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>(); for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) { final ThreadLocal<Object> threadLocal = entry.getKey(); final TtlCopier<Object> copier = entry.getValue(); threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get())); } return threadLocal2Value; }

都是通过集合 holder 与 threadLocalHolder 中获取值,看来主线程的TTL注册的TL都保存在Holder中,看看什么是Hodler

Holder 管理当前线程所有的TTL

注意上面的代码都是在TTLRunalbe 类中,而这个TTLRunalbe 类 是实例级别的,主子线程共享的对象。

而 Holder 类 是 类型为 InheritableThreadLocal 的 TransmittableThreadLocal 类中的一个静态类。 是一个包含了当前线程所有TTL引用的Set集合

就相当于Thread 中的 inheritableThreadLocals,只不过Holder 只保留了 Key 是TTL类型的Keys集合

管理维护holder 能够快速进行当前线程的快照导出,同时也可以

数据结构

 

java

体验AI代码助手

代码解读

复制代码

// Note about the holder: // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*). // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>. // 2.1 but the WeakHashMap is used as a *Set*: // the value of WeakHashMap is *always* null, and never used. // 2.2 WeakHashMap support *null* value. // 上面的注释总结来说就是 这个Map你可以看作是一个Set,因为只用到了Key值,value一直是为null private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() { @Override protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() { return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(); } @Override protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) { return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue); } };

首先Holder 是 InheritableThreadLocal类 意味着 这是本地线程变量,线程访问安全

变量的数据结构是 WeakHashMap 一种key为弱引用的Map 这里其实就是类似于JDK的ThreadLocalMapEntry,Entry 的 key 就是一个弱引用,而value 则是被Entry持有的强引用 (这也就是为什么 ThreadLocalMap 不执行remove的情况下会发生value内存泄漏的原因)

为什么使用 WeakHashMap? 相比是因为Key在被回收时 对应的value会自动清理而不存在内存泄漏风险,同时JDK并没有WeakHashSet 这种数据结构

(弱引用在GC时如果没有其他代码直接持有 TransmittableThreadLocal key 则K,V直接被回收,JVM的引用类型共有四种,强引用(也是在项目中主要的引用),软引用(FullGC清除),弱引用(GC清除,常用于缓存构建,caffein也使用了弱引用),虚引用(主要用于生产消费通知来释放系统资源功能))

总结

Holder 是 管理当前线程TTL的 集合,通过Holder可以快速生成当前线程的TTL快照,这个快照在子线程执行TTL任务时会传递下去并赋值给子线程的Holder

Holder 是如何进行维护的? (重要!!!)

holder 的 变化与任务的生命周期是绑定的,分为执行前,执行中与执行后。掌握了TTLRunable的流程应该知道任务执行的流程

  1. 执行前,捕获主线程的holder快照也就是capture,用于运行时的TTL赋值
  2. 执行中,子线程 先生成backup 备份数据,用于后续的TTL环境还原 然后从获取 的capture 将TTL环境设置在当前线程环境下
  3. 执行实际任务,如果任务执行期间有使用了新的TTL 也会将其加入holder
  4. 执行后,通过backup 进行还原数据,此时会清空任务期间加入的TTL以及 主线程传入的TTL

如果一个任务线程执行了一段时间的任意数量不同的任务然后等待,这个时候的线程holder 其实就是第一次执行任务时的主线程的 holder 环境,这是因为线程在第一次创建时 init 方法中 holder 重写了 childValue。而后续在执行不同任务时holder是不断变化的,但最终都会还原到线程初始化的状态

回到 TtlRunnable Run 方法
 

java

体验AI代码助手

代码解读

复制代码

@Override public void run() { // 此时我们已经分析完了captured的数据内容与来源 final Object captured = capturedRef.get(); // 安全检查,注意这里的CAS操作将capturedRef设置为了Null,所以TtlRunnable只能执行一次,后续执行时会触发 captured == null 抛出异常 if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } // replay (重现)结合最后面的 restore (恢复)看起来像是将执行任务前的TTL环境获取出来用户后续的恢复 final Object backup = replay(captured); try { // 真正的任务执行 runnable.run(); } finally { // 看起来像恢复TTL环境 restore(backup); } }

掌握了TtlRunablecaputerd 方法 继续往下看到 replay

 

java

体验AI代码助手

代码解读

复制代码

@NonNull public static Object replay(@NonNull Object captured) { final Snapshot capturedSnapshot = (Snapshot) captured; return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value)); }

那么接下来看看 复现replay() 与 还原restore的逻辑
首先是执行任务前 准备备份数据 replay()
 

java

体验AI代码助手

代码解读

复制代码

@NonNull private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) { HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) { TransmittableThreadLocal<Object> threadLocal = iterator.next(); // backup backup.put(threadLocal, threadLocal.get()); // clear the TTL values that is not in captured // avoid the extra TTL values after replay when run task // 如果子线程运行了不同的任务,那么holder可能会存在不同任务环境的TTL,因此这里做任务之间的隔离 if (!captured.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // set TTL values to captured setTtlValuesTo(captured); // call beforeExecute callback doExecuteCallback(true); return backup; }

  1. 遍历 holder 的 key 拿TTL 引用 然后 将引用与值构造 backup 备份
  2. 删除 holder 中 不存在于 captured 的TTL
  3. TTLvalue设置在captured
  4. 执行钩子函数(传入true则为调用前执行)
  5. 返回 backup 备份

可以思考一下为什么过滤 holder 中的TTL不在构造 backUp之前执行呢?

设置 快照值至当前的 TTL中 setTtlValuesTo(captured);
 

java

体验AI代码助手

代码解读

复制代码

private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) { for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) { TransmittableThreadLocal<Object> threadLocal = entry.getKey(); threadLocal.set(entry.getValue()); } }

代码比较简单, 不要忘记了 TransmittableThreadLocal 重写了 set 方法,会将其加入当前的holder

虽然TransmittableThreadLocal 继承了 InheritableThreadLocal 会自动赋值主线程的值 但是这也是仅仅发生 线程第一次创建时进行拷贝,对于池化线程 是需要在执行任务前重新赋值的 也就是为什么要执行setTtlValuesTo(captured);

接下来是还原备份数据 至当前线程中 restore
 

java

体验AI代码助手

代码解读

复制代码

public static void restore(@NonNull Object backup) { final Snapshot backupSnapshot = (Snapshot) backup; restoreTtlValues(backupSnapshot.ttl2Value); restoreThreadLocalValues(backupSnapshot.threadLocal2Value); }

直接看调用方法

 

java

体验AI代码助手

代码解读

复制代码

private static void restoreTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) { // call afterExecute callback doExecuteCallback(false); for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) { TransmittableThreadLocal<Object> threadLocal = iterator.next(); // clear the TTL values that is not in backup // avoid the extra TTL values after restore // 这里其实就是将holder还原至执行replay前的状态 if (!backup.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // restore TTL values setTtlValuesTo(backup); }

是不是乍一眼看非常熟悉,相比于replay方法的入参时快照,而还原的入参则是 backup 备份

还可以思考一下replay 的 iterator.remove(); 和 restore 的 iterator.remove(); 是一个意思吗?能否只保留一次 iterator.remove() 呢?

TIPS:
TransmittableThreadLocal 有两个钩子函数 分别为执行前和执行后,可以自行拓展逻辑

调用源码,通过参数控制是任务执行前执行还是执行后执行

 

java

体验AI代码助手

代码解读

复制代码

private static void doExecuteCallback(boolean isBefore) { for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { try { if (isBefore) threadLocal.beforeExecute(); else threadLocal.afterExecute(); } catch (Throwable t) { if (logger.isLoggable(Level.WARNING)) { logger.log(Level.WARNING, "TTL exception when " + (isBefore ? "beforeExecute" : "afterExecute") + ", cause: " + t.toString(), t); } } } }

总结

TransmittableThreadLocal 作为 InheritableThreadLocal 的增强版,主要完善了在池化线程执行任务时的生命周期,执行前,执行中与执行后的逻辑,其原理是通过包装任务来在执行真正任务前获取主线程的TTL的快照值,并且生成当前线程的TTL备份数据,将快照值设置在执行任务前。执行完任务后 根据生成的备份恢复任务线程的 TTL 值,其中细节还包括维护管理holder

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值