【Java】SpringBoot中实现多线程间的数据传递

前言

在实际开发中,难免遇到需要在父子线程之间传递一些数据,如用户信息,链路信息等等。

例如用户信息,通常会使用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();
            }
        };
    }
}

在实际开发中还可以设置其他的共享数据,比如SecurityContextRequestAttributes

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();
    }
}

这样我们就可以在创建线程的时候将值传递给子线程,从而实现了父子线程的数据传递。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值