前言
注:在看此篇文章前,你需要了解 ThreadLocal、InheritableThreadLocal
的原理。
这里先总体的介绍TransmittableThreadLocal
(下文以 ttl
作为简称)的原理再去分析一些核心的源码,旨在先有个整体的认识,再去详细了解源码。
整体介绍
复制父线程的 ttl 到 TtlRunnable
由于InheritableThreadLocal
是新建线程时复制父线程的本地变量到子线程,在线程池中由于线程只创建一次,因此无法复制新增的本地变量。
ttl
照着这个思路去找替代方案,在新建线程任务(Runnable)时将这些本地变量放到另一个类的属性字段中保存,这个类就是 TtlRunnable
。
下图的 holder 是一个 InheritableThreadLocal
,存的是当前线程的一个 WeakHashMap
key 为 ttl 对象 value 为 null,在这里可以暂时先忽略,后续再详细介绍。
将 TtlRunnable 保存的 ttl 复制到子线程
在执行 TtlRunnable
的 run()
方法时,再将属性中保存的值通过 TransmittableThreadLocal#set() 方法复制到子线程的 holder 中。需要注意的是 set() 方法并不只复制到 holder,还调用 super.set() 方法,这样相当于在子线程执行一次 ThreadLocal.set(),那么子线程就复制了父线程的本地变量。
子线程如何访问值
通过上一步,已完成从父线程将 ttl
复制到子线程,那就跟 InheritableThreadLocal
类似了,通过 get()
方法就能拿到当前子线程在父类中同一个 ttl
对象对应的值。
小结
父线程通过在新建线程任务时,将父线程所有的 ttl
先保存到 TtlRunnable
对象中,在运行任务时,再将 TtlRunnable
中的值再复制到子线程中,这样子线程就能够访问父线程中同一个 ttl
对象对应的值;这里的话如果不明白没关系,也是先给个整体的框架先了解下,后面再细分析一波就懂了。
需要注意的是,上面的步骤没有提到子线程 holder 的备份与恢复。
详细分析
我们以下面这个例子进行分析,下面代码演示了 TransmittableThreadLocal
的一种基本用法:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
// 父线程设置值
ttl.set("法外狂徒张三");
// 需要使用 TtlRunnable 包装线程任务 Runnable,完成复制
Runnable ttlRunnable = TtlRunnable.get(() -> {
// 获取同一个 ttl 对象在父子线程中的值是否一致
String value = ttl.get();
System.out.println(value);
});
executorService.submit(ttlRunnable);
executorService.shutdown();
}
现在先介绍下刚才说到的 holder:
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
,因为 Java 没有提供WeakHashSet
,所以用这个替代,而这个“Set”存的就是文章开头图的 ttl1
、ttl2
、ttl3
等;实现的两个方法其实就是避免在 get() 的时候空指针,从下面这段代码可以看出:
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
}
正菜开始
- 在执行
ttl.set("法外狂徒张三");
时,做了哪些?点进源码 (父线程中执行):
public final void set(T value) {
// ...
// 调用父类设置值,本质还是与 ThreadLocal 一致
super.set(value);
// 将当前 this 对象添加到 holder,为后续复制给子线程做准备
addThisToHolder();
// ...
}
// ....
private void addThisToHolder() {
// 先判断下,避免直接 put 消耗性能
if (!holder.get().containsKey(this)) {
// 当 Set 使用,所以 value 为 null
holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
}
}
这一步其实就做两件事,第一将调用 ThreadLocal#set() 方法设置本地变量值;第二将 ttl 添加到 holder 中。
- 再往下走,执行到
TtlRunnable.get(...);
这一步就开始复制父线程的ttl
对象到TtlRunnable
中 (父线程中执行):
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
// 父线程复制的 ttl 就存在这里
private final AtomicReference<Object> capturedRef;
// 线程任务
private final Runnable runnable;
// 略...
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
// 通过调用 capture() 完成父线程本地变量的获取
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
// 略...
}
// 略...
}
注意这里复制到 TtlRunnable
还是在父线程这里执行。通过 Transmitter#capture()
方法完成复制,并放到原子类中,至于为什么用原子类不是本文的重点,只要知道是为了保证在运行 run() 方法时保证线程安全就行。
下面我们看看 capture()
的具体实现:
public static class Transmitter {
// 略...
public static Object capture() {
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
// 复制父线程 ttl 的值
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
// 由于当前方法在父线程执行,直接通过 holder 可以拿到父线程的所有 ttl
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
// 复制父线程 threadlocal 的值
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;
}
}
// 存储父线程的 本地变量 值使用,就两个 Map
private static class Snapshot {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;
private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
this.ttl2Value = ttl2Value;
this.threadLocal2Value = threadLocal2Value;
}
}
经过上面的步骤,就完成了父线程复制到 TrlRunnable
保存。
- 再继续执行
executorService.submit(ttlRunnable);
提交任务到线程池,开始执行TrlRunnable
的 run() 方法
public void run() {
// 获取父线程保存的本地变量,这里为 Snapshot 对象
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
// * 这一步骤最重要,这里完成了备份跟设置本地变量到子线程中
final Object backup = replay(captured);
try {
runnable.run();
} finally {
// 最后恢复备份的值
restore(backup);
}
}
关于备份恢复,这里不做介绍有兴趣可以自己看源码,这里就着重讲下如何复制到子线程的
public static Object replay(@NonNull Object captured) {
final Snapshot capturedSnapshot = (Snapshot) captured;
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>();
// 因为现在在子线程里,所以这个 holder 是子线程原来的本地变量,需要备份它们
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
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 重点看这步,将父线程的值复制到子线程
setTtlValuesTo(captured);
// call beforeExecute callback
doExecuteCallback(true);
return backup;
}
private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
// 到这里,你可能忘了这个 ttlValues 是哪里来的,这里再说明下
// 这个值是从父线程的 holder 里面复制出来的,并存放到 TtlRunnable 对象中
// 这里对应的是总体介绍的第二步中的图
for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {
TransmittableThreadLocal<Object> threadLocal = entry.getKey();
// 这里明确下,set 是跟线程挂钩的,其实底层是放在线程的一个 Map 集合中
// key 就是 threadLocal,value 就是要设置的值。
// 这一步就表示,在子线程也设置一个与父线程使用同一个 key 的本地变量
// 那么在子线程通过这个 key 就能拿到父线程一样的值,这里的 key 就是 ttl 对象
threadLocal.set(entry.getValue());
}
}
- 最后在子线程通过
String value = ttl.get();
就能拿到父线程的值了
public final T get() {
// 因为 set 的时候也是保存在线程的 threadlocalMap 中,直接通过 super.get() 就能拿到值
T value = super.get();
if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
return value;
}
总结
本文一开始总体的介绍了下 TransmittableThreadLocal
的原理,先了解个大概的流程,后面再通过实际的源码分析进一步说明是如何复制的。但这里由于篇幅有限,并没有分析其他除复制外的内容,感兴趣的读者可以自己去加以分析。
如有任何错误,欢迎指出!