前言
在实际开发中,难免遇到需要在父子线程之间传递一些数据,如用户信息,链路信息等等。
例如用户信息,通常会使用ThreadLocal 存放以保证线程隔离。例如:
public class UserContext {
private static final ThreadLocal<User> loginUserThreadLocal = new ThreadLocal<>();
public static User getUser(){
return loginUserThreadLocal.get();
}
public static void setUser(User user){
loginUserThreadLocal.set(user);
}
public static void clear(){
loginUserThreadLocal.remove();
}
}
然后在处理用户请求的开始,我们可以设置 ThreadLocal 中的用户信息。例如,可以在拦截器或过滤器中这么做:
public class UserRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
User user = ... // 从请求中获取或验证用户信息
UserContext.setUser(user);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserContext.clear(); // 清除ThreadLocal中的数据,防止内存泄漏
}
}
接着我们就可以在应用程序的任何地方,只要是在同一个线程中,都可以通过 UserContext 轻松访问当前用户的信息
public void doSomething() {
User user = UserContext.getUser();
// 使用 user 进行一些操作...
}
这样写的话,只能在当前线程下获取到用户信息,但是如果有些业务情景需要使用到多线程或异步请求的话,子线程就不能通过UserContext直接去获取用户信息了,于是就需要实现父子线程间的数据传递。
1.手动设置
既然每次开启新线程的时候,新线程不会携带主线程的信息,那么可以先从主线程中先获取相应信息,再设置到子线程中。
public void handlerAsync() {
//1. 获取父线程的用户信息
User user = UserContext.getUser();
log.info("父线程的值:{}",UserContext.getUser());
CompletableFuture.runAsync(()->{
//2. 设置子线程的值,复用
UserContext.set(user);
log.info("子线程的值:{}",UserContext.getUser());
});
}
虽然能够实现目的,但是每次开异步线程都需要手动设置,重复代码太多,不够优雅。
2.线程池设置TaskDecorator
TaskDecorator是一个执行回调方法的装饰器,主要应用于传递上下文,或者提供任务的监控/统计信息。
TaskDecorator是一个接口,首先需要去实现它
/**
* @description 上下文装饰器
*/
public class ContextTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
//获取父线程的User
User user = UserContext.getUser();
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
// 将主线程的请求信息,设置到子线程中
UserContext.setUser(user);
RequestContextHolder.setRequestAttributes(requestAttributes);
// 执行子线程,这一步不要忘了
runnable.run();
} finally {
// 线程结束,清空这些信息,否则可能造成内存泄漏
UserContext.clear();
}
};
}
}
在实际开发中还可以设置其他的共享数据,比如SecurityContext,RequestAttributes等
TaskDecorator需要结合线程池使用,实际开发中异步线程建议使用线程池,只需要在对应的线程池配置一下,代码如下:
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
poolTaskExecutor.setCorePoolSize(xx);
poolTaskExecutor.setMaxPoolSize(xx);
// 设置线程活跃时间(秒)
poolTaskExecutor.setKeepAliveSeconds(xx);
// 设置队列容量
poolTaskExecutor.setQueueCapacity(xx);
//设置TaskDecorator,用于解决父子线程间的数据复用
poolTaskExecutor.setTaskDecorator(new ContextTaskDecorator());
poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return poolTaskExecutor;
}
此时业务代码就不需要去设置子线程的值,直接使用即可,代码如下:
public void handlerAsync() {
log.info("父线程的用户信息:{}", UserContext.getUser());
//执行异步任务,需要指定的线程池
CompletableFuture.runAsync(()-> log.info("子线程的用户信息:{}", UserContext.getUser()),taskExecutor);
}
这里使用的是CompletableFuture执行异步任务,使用@Async这个注解同样是可行的。
3.TransmittableThreadLocal
TransmittableThreadLocal是阿里开源的工具,弥补了InheritableThreadLocal的缺陷,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
- 首先添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.2</version>
</dependency>
- 改造UserContent
/**
* @description 用户上下文信息
*/
public class UserContext {
private static final TransmittableThreadLocal<User> loginUserThreadLocal = new TransmittableThreadLocal<>();
public static User get(){
return loginUserThreadLocal.get();
}
public static void set(User user){
loginUserThreadLocal.set(user);
}
public static void clear(){
loginUserThreadLocal.remove();
}
}
这样我们就可以在创建线程的时候将值传递给子线程,从而实现了父子线程的数据传递。