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的ThreadLocalMap
的Entry
,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的流程应该知道任务执行的流程
- 执行前,捕获主线程的
holder
快照也就是capture
,用于运行时的TTL
赋值 - 执行中,子线程 先生成
backup
备份数据,用于后续的TTL环境还原
然后从获取 的capture
将TTL
环境设置在当前线程环境下 - 执行实际任务,如果任务执行期间有使用了新的
TTL
也会将其加入holder
中 - 执行后,通过
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); } }
掌握了TtlRunable
的caputerd
方法 继续往下看到 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; }
- 遍历
holder 的 key
拿TTL
引用 然后 将引用与值构造backup
备份 - 删除
holder
中 不存在于captured
的TTL
- 将
TTL
的value
设置在captured
中 - 执行钩子函数(传入true则为调用前执行)
- 返回
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