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 的初始化
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
具体初始化方法,没有特别需要注意的
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
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
而 inheritableThreadLocals 则是在 Thread.init() 方法时从主线程进行赋值构造
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
createInheritedMap 方法
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并不能然你选择使用深拷贝而只能使用浅拷贝
**测试代码 **
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());
}
结果
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
// 在主线程中设置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 任务
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 方法( 这时候执行线程已经是子线程了!)
@Override
public void run() {
// 虽然不知道这个capture具体是什么但是大致能够猜到这个就是从主线程保存的TTL值
final Object captured = capturedRef.get();
// 安全检查,如果releaseTtlValueReferenceAfterRun在get方法中入参为true(默认false),那么这个任务无法执行第二次,这里的CAS操作将capturedRef设置为了Null,后续执行时会触发 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()
@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构造方法(主线程执行)
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()
public static Object capture() {
// 这里的第二个参数是指主线程通过`TtlThreadLocal.registerThreadLocal` 注册的普通TL
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
Ttl 与 普通Tl 快照生成
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 能够快速进行当前线程的快照导出,同时也可以
数据结构
// 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时如果没有其他代码直接持有
TransmittableThreadLocalkey 则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 方法
@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
@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()
@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);
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
public static void restore(@NonNull Object backup) {
final Snapshot backupSnapshot = (Snapshot) backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}
直接看调用方法
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 有两个钩子函数 分别为执行前和执行后,可以自行拓展逻辑
调用源码,通过参数控制是任务执行前执行还是执行后执行
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