概述
在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
ThreadLocal使用场景
•针对线程不安全的例如SimpleDateFormat使用时能够支持多线程状态下的安全使用。同时不需要实例化过多实例和使用锁。
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
•上下文调用中屏蔽过多的方法栈中的传参(例如用户信息,客户端信息等存入ThreadLocal
,那么当前线程在任何地方需要时,都可以使用)
public class ClientThreadLocal {
private static final ThreadLocal<Map<String, Object>> THREAD_LOCAL = ThreadLocal.withInitial(Maps::newHashMap);
public static void set(Map<String, Object> clientMap) {
THREAD_LOCAL.set(clientMap);
}
public static void remove() {
THREAD_LOCAL.remove();
}
/**
* 此方法可以支持在当前线程内任意地方获取客户端信息
* 而不需要将clientInfo一直传递在上下文中
* @return
*/
public static Map<String, Object> get() {
return THREAD_LOCAL.get();
}
}
InheritableThreadLocal使用场景
private final static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private final static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("threadLocal");
Runnable runnable1 = () -> System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
new Thread(runnable1).start();
inheritableThreadLocal.set("inheritableThreadLocal");
Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());
new Thread(runnable2).start();
}
很明显ThreadLocal的子类InheritableThreadLocal在ThreadLocal的基础上,解决了和线程相关的副本从父线程向子线程传递的问题。
TransmittableThreadLocal使用场景
InheritableThreadLocal的原理是通过创建线程的时候,会把父线程当前的inheritableThreadLocals拷贝过去。但是业务会存在很多使用线程池的场景,线程池的线程是池化和复用的。这种情况下,运行一段时间线程池,会导致线程一直获取不到最新的InheritableThreadLocal。从而导致部分信息丢失,TransmittableThreadLocal就是用来解决这个问题的。
package com.jd.app.lambda.test.service;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
public class TempTest4 {
private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
private static TransmittableThreadLocal transmittableThreadLocal = new TransmittableThreadLocal();
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,2,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
public static void main(String[] args) {
Runnable runnable2 = () -> System.out.println(Thread.currentThread().getName()+":"+inheritableThreadLocal.get());
inheritableThreadLocal.set(Thread.currentThread()+"inheritableThreadLocal");
threadPoolExecutor.submit(runnable2);
threadPoolExecutor.submit(runnable2);
inheritableThreadLocal.set(Thread.currentThread()+"again inheritableThreadLocal");
/**
* 这里很明显没有覆盖
* 输出还是第一次set进去的值
*/
threadPoolExecutor.submit(runnable2);
Runnable runnable3 = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
transmittableThreadLocal.set(Thread.currentThread()+"transmittableThreadLocal");
threadPoolExecutor.submit(TtlRunnable.get(runnable3));
threadPoolExecutor.submit(TtlRunnable.get(runnable3));
transmittableThreadLocal.set(Thread.currentThread()+"again transmittableThreadLocal");
/**
* 这里覆盖了 异步执行的时候能够正常输出新值
*/
threadPoolExecutor.submit(TtlRunnable.get(runnable3));
}
}
TransmittableThreadLocal使用方式
一.侵入式代码改造
1.Runnable
Runnable runnable = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
transmittableThreadLocal.set(Thread.currentThread()+"runnable transmittableThreadLocal");
threadPoolExecutor.submit(TtlRunnable.get(runnable));
2.Callable
Callable callable = () -> {
System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
return null;
};
transmittableThreadLocal.set(Thread.currentThread()+"callable transmittableThreadLocal");
threadPoolExecutor.submit(TtlCallable.get(callable));
3.线程池
//普通线程池
Runnable runnable = () -> System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
Callable callable = () -> {
System.out.println(Thread.currentThread().getName()+":"+transmittableThreadLocal.get());
return null;
};
transmittableThreadLocal.set(Thread.currentThread()+"TtlExecutors transmittableThreadLocal");
ExecutorService executorService = TtlExecutors.getTtlExecutorService(threadPoolExecutor);
executorService.submit(runnable);
executorService.submit(callable);
//stream并行流自定义forkjoinpool
List<Integer> integerList= IntStream.range(1,1000).boxed().collect(Collectors.toList());
ForkJoinPool customThreadPool = new ForkJoinPool(4);
ExecutorService executorService = TtlExecutors.getTtlExecutorService(customThreadPool);
Integer actualTotal = executorService.submit(
() -> integerList.parallelStream().reduce(0, Integer::sum)).get();
//CompletableFuture下自定义forkjoinpool
CompletableFuture future3 = CompletableFuture.runAsync(()->{
System.out.println("999");
},TtlExecutors.getTtlExecutorService(threadPoolExecutor));
二.非侵入式agent改造
根据jar包地址,启动脚本添加相关路径jar。
-javaagent:path/to/transmittable-thread-local-2.x.y.jar
添加并启动正常后,可以不修改业务代码修饰任务或者线程池,直接使用。默认修饰了jdk自带的相关任务或者线程池如下。
1.java.util.concurrent.ThreadPoolExecutor
和 java.util.concurrent.ScheduledThreadPoolExecutor
2.java.util.concurrent.ForkJoinTask
(对应的执行器组件是java.util.concurrent.ForkJoinPool
)
3.java.util.TimerTask
的子类(对应的执行器组件是java.util.Timer
)
相关agent技术文档说明:
javaagent: JavaAgent 技术原理及简单实现 - 简书
ttl使用javaagent: TransmittableThreadLocal通过javaAgent技术实现线程传递(并且支持ForkJoin) - 简书
异步执行上下文传递标准范式
在上面的TransmittableThreadLocal使用场景说明的时候,已经确认到线程池的线程是复用的。所以按照InheritableThreadLocal的想法在创建线程的时候操作相关需要的上下文信息是不合理的。使用线程池的时候,需要提交任务。所以在提交任务的时候,把上下文信息打包给到任务是合理的。
一.异步执行传递上下文标准范式
private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static <T> CompletableFuture<T> invokeToCompletableFuture(Supplier<T> supplier) {
contextHolder.set("main");
// 第一步 主线程获取上下文,传递给任务暂存。
String context = contextHolder.get();
// 异步任务逻辑执行
return CompletableFuture.supplyAsync(() -> {
// 第二步 异步执行线程将原有上下文取出,暂时保存。
String origin = contextHolder.get();
try {
contextHolder.set(context);
// 第三步 执行异步任务 这里已经是异步执行的时候设置的context值了
return supplier.get();
} finally {
// 第四步 原来的异步线程值重新放回去
contextHolder.set(origin);
log.info(origin);
}
});
}}
二.基于标准范式的任务包装
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,3,3000, TimeUnit.MILLISECONDS,new ArrayBlockingQueue(100));
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public final static class PackageRunnable implements Runnable {
private Runnable runnable;
private String mainContext;
public PackageRunnable(Runnable runnable,String mainContext) {
this.runnable = runnable;
this.mainContext = mainContext;
}
public PackageRunnable(Runnable runnable) {
// 修饰原有的任务,并保存当前线程的值
this(runnable, threadLocal.get());
}
//异步执行
public void run() {
//异步执行线程将原有上下文取出,暂时保存。
String originalContext = threadLocal.get();
try {
//主线程传递过来的上下文设置进来。
threadLocal.set(mainContext);
runnable.run();
} finally {
//将原有上下文设置刷新回去。
threadLocal.set(originalContext);
}
}
}
public final void execute(Runnable runnable) {
// 递交给真正的执行线程池前,对任务进行修饰
threadPoolExecutor.execute(pack(runnable));
}
protected static final Runnable pack(Runnable runnable) {
return new PackageRunnable(runnable);
}
三.基于标准范式的线程池包装
static PackageThreadPool packageThreadPool = new PackageThreadPool(3,3,3000,3000);
public static class PackageThreadPool extends ThreadPoolExecutor {
public PackageThreadPool(int corePoolSize, int maximumPoolSize, int queueSize, long keepAliveTime) {
super(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue(queueSize <= 0 ? 10: queueSize), Executors.defaultThreadFactory());
}
//包装线程池 本质也是包装任务
@Override
public void execute(Runnable runnable) {
super.execute(pack(runnable));
}
}
public static void main(String[] args) {
Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
packageThreadPool.submit(runnable);
}
TransmittableThreadLocal源码解析
TransmittableThreadLocal官方文档时序图
public final class TtlCallable<V> implements Callable<V>, TtlWrapper<Callable<V>>, TtlEnhanced, TtlAttachments {
// 保存父线程的 ThreadLocal 快照
private final AtomicReference<Object> capturedRef;
// 实际执行任务
private final Callable<V> callable;
// 判断是否执行完,清除任务所保存的 ThreadLocal 快照
private final boolean releaseTtlValueReferenceAfterCall;
private TtlCallable(@NonNull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
// 1.创建时, 从 Transmitter 抓取快照
this.capturedRef = new AtomicReference<Object>(capture());
this.callable = callable;
this.releaseTtlValueReferenceAfterCall = releaseTtlValueReferenceAfterCall;
}
@Override
public V call() throws Exception {
//1.获取父线程传递下来的上下文。临时保存在captured。
Object captured = capturedRef.get();
// 如果 releaseTtlValueReferenceAfterCall 为 true,则在执行线程取出快照后清除。
if (captured == null || releaseTtlValueReferenceAfterCall && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after call!");
}
// 2.使用 Transmitter 将快照重做到当前执行线程,并将原来的值取出 这里将父线程传递的captured也就是
//上下文
// 并返回异步线程原来持有的上下文
Object backup = replay(captured);
try {
// 3.执行任务 执行任务过程使用的就会使用父线程传递下来的上下文
return callable.call();
} finally {
// 4.Transmitter 重新将异步线程原来持有的上下文放到异步线程里
restore(backup);
}
}
}
可以看到相关源码符合标准范式,下面具体分析三个主要方法分别对应Transmittee的capture,replay,restore方法
1.capture
public HashMap<TransmittableThreadLocal<Object>, Object> capture() {
final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = newHashMap(holder.get().size());
for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
//可以看到这里将holder下所有的TransmittableThreadLocal作为key TransmittableThreadLocal当前线程持有的value作为
//value进行重新put。当前线程也就是父线程。
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
2.replay
public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
final HashMap<TransmittableThreadLocal<Object>, Object> backup = newHashMap(holder.get().size());
for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object> threadLocal = iterator.next();
// backup 这里将holder下所有的TransmittableThreadLocal以及当前异步线程所持有的value重新赋值
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();
}
}
// 替换父线程传递下来的value值到当前异步线程
setTtlValuesTo(captured);
doExecuteCallback(true);
return backup;
}
3.restore
public void restore(@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
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 刷新当前异步线程传递下来的上下文值到当前异步线程
setTtlValuesTo(backup);
}
4.holder的设计
从上面的相关逻辑可以看到holder这个设计,是用来注册所有的TransmittableThreadLocal。那么为什么需要这个holder来注册所有的TransmittableThreadLocal呢?
一.支持注册多个TransmittableThreadLocal
比如说单个请求同时需要传递客户端信息,用户登陆信息等等,那么这里就需要声明一个以上的TransmittableThreadLocal,并且异步上下文传递的时候需要将所有的TransmittableThreadLocal都进行传递。
二.线程级别的缓存
我们知道ThreadLocal和InheritableThreadLocal怎么做到线程级别的缓存,是通过Thread类的属性来绑定的。setValue的时候通过Thread的map来存储ThreadLocal和InheritableThreadLocal分别作为key的value值。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
显然我们不可能拓展Thread这个类,成本太高且不可控。所以设计这个holder来作为一个线程级别的缓存缓存当前线程请求下所有的TransmittableThreadLocal。同样的在setValue的时候将相关TransmittableThreadLocal注册到这个static的holder上。
static用来保证当前线程 无论创建多少次TransmittableThreadLocal,维护的都是同一个缓存holder。
public final void set(T value) {
if (!disableIgnoreNullValueSemantics && value == null) {
// may set null to remove value
remove();
} else {
super.set(value);
addThisToHolder();
}
}
//注册到holder
private void addThisToHolder() {
if (!holder.get().containsKey(this)) {
holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
}
}
可以看到这个holder作为一个WeakHashMap的key是当前TransmittableThreadLocal对象。
private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
return new WeakHashMap<>();
}
@Override
protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
return new WeakHashMap<>(parentValue);
}
};
三.弱引用机制
为了确保不会造成内存泄漏,TransmittableThreadLocal作为key被弱引用(发生gc就会回收该TransmittableThreadLocal)
其实这里的概念和ThreadLocal以及InheritableThreadLocal里面的设计是一致的。
Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(threadLocal对象)和value
Thread -> InheritableThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(inheritableThreadLocal对象)和value
Thread -> holder.ThreadLocalMap -> Entry[] -> Enrty -> key(transmittableThreadLocal对象)和value
如上,如果是强引用的话,Thread很难销毁的情况下,会一直持有相关的对象。就会存在内存泄漏。
所以弱引用情况下,gc的时候就会直接回收所在类的ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal。
如果这些ThreadLocal,InheritableThreadLocal,TransmittableThreadLocal不存在被持有的情况下。
弱引用是一种设计上的弥补措施。用来防止因为线程一直不销毁的情况下发生的key的内存泄漏。
5.remove机制
既然上面的key不会发生内存泄漏,为什么会有remove方法可以调用呢?
是不是 Key 持有的是 threadlocal 对象的弱引用就一定不会发生内存泄漏呢?
使用不当情况下,仍然会发生内存泄漏:
当 threadlocal 使用完后,将栈中的 threadlocal 变量置为 null,threadlocal 对象下一次 GC 会被回收,那么 Entry 中的与之关联的弱引用 key 就会变成 null,如果此时当前线程还在运行,那么 Entry 中的 key 为 null 的 Value 对象并不会被回收(存在强引用),这就发生了内存泄漏,当然这种内存泄漏分情况,如果当前线程执行完毕会被回收,那么 Value 自然也会被回收, 但是如果使用的是线程池呢,线程跑完任务以后放回线程池(线程没有销毁,不会被回收),Value 会一直存在,这就发生了内存泄漏。
ThreadLocal如何更好的降低内存泄漏的风险呢?
ThreadLocal 为了降低内存泄露的可能性,在 set,get,remove 的时候都会清除此线程 ThreadLocalMap 里 Entry 数组中所有 Key 为 null 的 Value。所以,当前线程使用完 threadlocal 后, 我们可以通过调用 ThreadLocal 的 remove 方法进行清除从而降低内存泄漏的风险。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//value设置为空,防止泄漏。
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
另外remove方法可以防止串上下文。可以在使用过后强制remove,下次该线程执行的时候有自己的上下文信息。
个人结论
个人愚见,TransmittableThreadLocal的使用是有成本的,它只适合异步执行的时候上下文的传递,解决线程池场景下ThreadLocal和InheritableThreadLocal不能正确传递上下文的问题。
但是并不适合传递之后的变量涉及到并发修改的场景。比如同一请求下,传递到异步线程后,该异步线程发起多线程并发修改这个变量,收集信息。这个是不合理的。
另外改造成本,包括
如果是agent改造,需要注意版本的升级,比如启动脚本和maven项目引用的版本后续不一致,没有覆盖的线程池的场景,需要代码中另做改造。
如果是代码改造,需要注意识别并改造所有涉及到线程池的场景,包括业务线程池,Hystrix这类中间件线程池,jdk并行流,CompletableFuture等涉及到的ForkJoinPool等线程池。
参考文档: