【TransmittableThreadLocal】TransmittableThreadLocal的实现机制和原理
1 前言
前面我看过了 ThreadLocal的实现机制和原理 以及 InheritableThreadLocal的实现机制和原理 两种类型的 ThreadLocal,前者是普通的,后者是在前者的基础上套了一层父子线程关系,当使用后者的时候,会在线程创建的时候,浅拷贝一份父线程的变量值。那么今天空了,我来看看另外一种 ThreadLocal:TransmittableThreadLocal。
2 TransmittableThreadLocal
2.1 TransmittableThreadLocal 的认识
TransmittableThreadLocal(TTL)是阿里巴巴开源的一个 Java 库,用于解决 ThreadLocal 在多线程环境下的一些问题,尤其是在使用线程池等场景下可能出现的问题。
与普通的 ThreadLocal 不同,TTL 具有以下特点:
(1)线程池透传性:在使用线程池执行任务时,TTL 可以透传 ThreadLocal 的值,确保后续线程能够正确访问前线程设置的 TransmittableThreadLocal 变量值。
(2)线程池隔离性:TTL 在多线程环境下能够确保每个线程都有独立的 TransmittableThreadLocal值,避免了线程池重用线程时可能出现的数据污染问题,比如线程池执行前从父线程继承的变量,不管是执行中变没变,下次执行任务的时候,还是会和父线程保持一致。
(3)资源自动清理:TTL 支持自动清理 TransmittableThreadLocal 值,避免了可能导致内存泄漏的问
题。
(4)兼容性:TTL 兼容原生 ThreadLocal 的语法和用法,可以直接替换原生 ThreadLocal 使用,而无需修改现有代码。
2.2 TransmittableThreadLocal 的使用
TTL 通常用于需要在线程池中执行任务,并且需要在任务之间传递 ThreadLocal 值的场景。
例如,在 Web 应用中,可能需要在异步任务中访问当前用户的会话信息,而使用 TTL 可以确保子线程能够正确访问父线程设置的会话信息。
TransmittableThreadLocal在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能(把任务提交给线程池时的ThreadLocal值传递到任务执行时),解决异步执行时上下文传递的问题。
我们来简单体验一下:
// TTL private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>(); public static void main(String[] args) { // 父线程设置变量 1 tl.set(1); new Thread(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }).start(); new Thread(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }).start(); }
这么一看貌似跟我们的 ITL 没什么区别是吧。我们来看一个线程池的例子:
public class Demo { private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>(); private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor( 2, 2, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100) ); public static void main(String[] args) { // 父线程设置变量 1 tl.set(1); poolExecutor.execute(() -> { // 更改当前线程中的值 tl.set(2); System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); poolExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); poolExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); poolExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); } }
我们看普通情况下,当某个线程改变了 TTL 的值后,下次该线程执行任务的时候,TTL 的值就是改变后的了。这里需要引入一下 TTL 里的线程池,我们再看:
public class Demo { private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>(); private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor( 2, 2, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100) ); private static final Executor ttlExecutor = TtlExecutors.getTtlExecutor(poolExecutor); public static void main(String[] args) { // 父线程设置变量 1 tl.set(1); ttlExecutor.execute(() -> { // 更改当前线程中的值 tl.set(2); System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); ttlExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); ttlExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); ttlExecutor.execute(() -> { System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get())); }); } }
看是不是,1号线程改变了 TTL 的值,下次执行的时候还是和父线程的值一致的。注意下边的这个例子我们的线程池是用TTL里的线程池包装了一层哈(这样才能发挥TTL的效果)。下面我们就来解析一下其内部原理,看看TTL是怎么完成对ITL的优化的。
2.3 源码分析
TTL 本质上是一个InheritableThreadLocal,意味着TTL具备ITL的功能:
// TTL继承关系 public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {
具体值是保存在线程内部的inheritableThreadLocals,不涉及线程池时可以当成 InheritableThreadLocal使用。
2.3.1 重要属性 holder
TransmittableThreadLocal重要核心属性holder:
// holder 本质上是一个 InheritableThreadLocal // 内部的值的类型是 WeakHashMap<TransmittableThreadLocal<Object>, ?> // 这个 WeakHashMap 被当成一个 Set 使用,这个WeakHashMap的value永远是null, 不会被使用到,key 存的就是 TTL的对象 private static 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的作用:存放的是所有和当前线程产生关联的 TransmittableThreadLocal,通过holder可以找到Java进程中所有的TransmittableThreadLocal(即使用WeakHashMap收集线程中所有的 TransmittableThreadLocal)。
ThreadLocal.ThreadLocalMap和WeakHashMap的区别?
相同点:ThreadLocalMap.Entry和WeakHashMap都继承弱引用,弱引用的reference都指向key :ThreadLocalMap.Entry:
不同点:ThreadLocalMap是一个用移位解决hash冲突的简易Map,而WeakHashMap是一个用链表解决hash冲突的简易Map;
2.3.2 重要方法
下面的方法均属于TTL类:
@Override public final T get() { T value = super.get(); if (disableIgnoreNullValueSemantics || null != value) addThisToHolder(); return value; } @Override public final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { // may set null to remove value remove(); } else { super.set(value); addThisToHolder(); } } @Override public final void remove() { // 从holder持有的map对象中移除该 ttl removeThisFromHolder(); super.remove(); } private void removeThisFromHolder() { holder.get().remove(this); } private void addThisToHolder() { if (!holder.get().containsKey(this)) { // 从 holder 中当前线程持有的map 中添加 ttl holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value. } }
TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过上边的例子除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以。
2.3.3 线程池相关重要方法
下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作(这一部分是TTL支持线程池传递的核心部分):首先打开包装类,看下execute方法在执行时做了些什么。
// 此方法属于线程池包装类ExecutorTtlWrapper @Override public void execute(@NonNull Runnable command) { // 这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行 executor.execute(TtlRunnable.get(command)); } // 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类 @Nullable public static TtlRunnable get(@Nullable Runnable runnable) { return get(runnable, false, false); } // 对应上面的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!"); } // 最终的初始化 return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun); } // 对应上面的TtlRunnable方法 private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) { // 这里就是重点了 这里将捕获后的父线程本地变量存储在当前对象的capturedRef里 this.capturedRef = new AtomicReference<Object>(capture()); this.runnable = runnable; this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun; } // 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter @Nonnull public static Object capture() { Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>(); for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) { // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象 captured.put(threadLocal, threadLocal.copyValue()); } // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集 return captured; } // 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法) private T copyValue() { return copy(get()); } protected T copy(T parentValue) { return parentValue; }
结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给Rannable包装类里的capturedRef属性,那么在接下来线程执行的时候,也就是run方法里,大概率会将这些捕获到的值赋给子线程的holder赋对应的TTL值,我们继续往下看Rannable包装类里的run方法是怎么实现的:
// run方法属于Rannable的包装类TtlRunnable @Override public void run() { // 获取由之前捕获到的父线程变量集 Object captured = capturedRef.get(); if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) { throw new IllegalStateException("TTL value reference is released after run!"); } // 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值 // backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复) Object backup = replay(captured); try { // 执行异步逻辑 runnable.run(); } finally { // 这个方法就是用来恢复原有值的 restore(backup); } }
根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。
下面来具体看这俩方法:
// 下面的方法均属于TTL的静态内部类Transmittable @Nonnull public static Object replay(@Nonnull Object captured) { @SuppressWarnings("unchecked") // 使用此线程异步时捕获到的父线程里的本地变量值 Map<TransmittableThreadLocal<?>, Object> capturedMap =(Map<TransmittableThreadLocal<?>, Object>) captured; //当前线程原生的本地变量,用于使用完线程后恢复用 Map<TransmittableThreadLocal<?>, Object> backup = newHashMap<TransmittableThreadLocal<?>, Object>(); //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量 for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用 backup.put(threadLocal, threadLocal.get()); /** * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉 * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用 * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程 * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量, * 意味着很可能会影响到任务里取值的准确性。 * * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时 * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过 * 该tl对象取到值2,不符合预期 */ if (!capturedMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值) setTtlValuesTo(capturedMap); // 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述 doExecuteCallback(true); return backup; } // 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的 public static void restore(@Nonnull Object backup) { @SuppressWarnings("unchecked") Map<TransmittableThreadLocal<?>, Object> backupMap =(Map<TransmittableThreadLocal<?>, Object>) backup; // call afterExecute callback doExecuteCallback(false); // 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量) for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) { Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next(); TransmittableThreadLocal<?> threadLocal = next.getKey(); /** * 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同? * 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值) */ if (!backupMap.containsKey(threadLocal)) { iterator.remove(); threadLocal.superRemove(); } } // 同样调用这个方法,进行值的恢复 setTtlValuesTo(backupMap); } // 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法 private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) { for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) { @SuppressWarnings("unchecked") TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey(); threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现 } }
好啦,到这里基本上把TTL比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:
2.4 应用场景
TransmittableThreadLocal用来实现线程间的参数传递,经典应用场景如下:
(1)分布式跟踪系统 或 全链路压测(即链路打标)
(2)日志收集记录系统上下文
(3)Session级 Cache
(4)应用容器或上层框架跨应用代码给下层 SDK传递信息
2.5 ThreadLocal、InheritableThreaLocal与TransmittableThreadLocal的比较
ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal在Java中都是用于处理线程局部变量的工具,但它们在使用场景和特性上有所不同。
ThreadLocal是Java中一个非常重要的线程技术,它为每个线程提供了它自己的变量副本,使得线程间无法相互访问对方的变量,从而避免了线程间的竞争和数据泄露问题。适用于需要在线程内部存储和获取数据,且不希望与其他线程共享数据的场景。
(2)InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的一个子类,它包含了ThreadLocal的所有功能,并扩展了ThreadLocal的功能。 允许父线程中的InheritableThreadLocal变量的值被子线程继承。当创建一个新的线程时,这个新线程可以访问其父线程中InheritableThreadLocal变量的值。
(3)TransmittableThreadLocal
TransmittableThreadLocal是阿里巴巴开源的一个框架,用于解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题。能够在多线程传递中保持变量的传递性,确保在父线程和子线程之间正确传递ThreadLocal变量。
简单的来说就是:ThreadLocal适用于线程内部的数据存储和访问,确保数据在线程间的隔离。InheritableThreadLocal适用于需要在父线程和子线程间传递数据的场景,实现数据的继承。TransmittableThreadLocal则是为了解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题,实现数据的跨线程传递。
3 小结
好啦,本节就里看到这里哈,有理解不对的地方还请指正哈。