Java在使用多线程(结合线程池)异步处理时,异步线程获取不到原线程的上下文对象

11 篇文章 0 订阅

0. 前言

在 Java 中,多线程的应用场景比较多,例如以下几种情况:

  • ElasticSearch 批量导入数据(具体可以参考我的另一篇博文:在SpringBoot项目中使用多线程(配合线程池)加快从MySQL导入数据到ElasticSearch的速度-CSDN博客
  • 数据汇总:例如,在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息,这三块信息都在不同的微服务中进行实现的,我们可以利用多线程并行地查询数据,结合 Future 接口实现数据汇总,从而加快查询数据的接口
  • 异步调用:在很多软件中都会有搜索功能,而且这些软件会保存你的搜索记录,我们在实现搜索功能的时候,可以将保存搜索记录操作从搜索功能中利用多线程分离出来,提高搜索接口的响应速度

但是,在使用多线程时可能会引发一些意想不到的问题,比如本文将要介绍的子线程获取不到父线程上下文对象的问题

1. 准备工作

在项目开发中,我们一般会在拦截器中解析出用户信息,接着将用户信息存放在 ThreadLocal 中

记得在SpringBoot启动类上添加@Async注解

1.1 User.java(实体类)

public class User {

    private Integer userId;

    private String username;

    private String password;

    public User() {

    }

    public User(Integer userId, String username, String password) {
        this.userId = userId;
        this.username = username;
        this.password = password;
    }

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "userId='" + userId + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

}

1.2 ThreadPoolConfig.java(线程池配置)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class ThreadPoolConfig {

    /**
     * 核心线程池大小
     */
    private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;

    /**
     * 最大可创建的线程数
     */
    private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 1;

    /**
     * 阻塞队列队列最大长度
     */
    private static final int QUEUE_CAPACITY = 1000;

    /**
     * 线程池中救急线程的存活时间
     */
    private static final int KEEP_ALIVE_SECONDS = 500;

    /**
     * 通过@Bean注解,将任务执行器(TaskExecutor)实例注入到Spring上下文中
     *
     * @return Executor
     */
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        // 创建线程池任务执行器实例
        ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
        // 设置核心线程池大小
        threadPoolExecutor.setCorePoolSize(CORE_POOL_SIZE);
        // 设置最大线程池大小
        threadPoolExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        // 设置队列容量
        threadPoolExecutor.setQueueCapacity(QUEUE_CAPACITY);
        // 设置线程空闲时间
        threadPoolExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
        // 设置拒绝策略为AbortPolicy,当线程池无法接受新任务时抛出异常
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 设置线程名称前缀为"task-thread-"
        threadPoolExecutor.setThreadNamePrefix("task-thread-");
        // 初始化线程池任务执行器
        threadPoolExecutor.initialize();
        // 返回线程池任务执行器实例
        return threadPoolExecutor;
    }

}

1.3 LoginInterceptor.java(登录拦截器)

import cn.edu.scau.common.UserContext;
import cn.edu.scau.pojo.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
        if (handler instanceof HandlerMethod) {
            String token = request.getHeader("token");
            User currentUser = getUserInfoByToken(token);
            UserContext.setUserContext(currentUser);
            return true;
        }

        return true;
    }

    @Override
    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception exception) {
        UserContext.clearUserContext();
    }

    /**
     * 从 Token 中解析出用户信息
     *
     * @param token String
     * @return user
     */
    private User getUserInfoByToken(String token) {
        // 省略从 Token 中解析出用户信息的步骤
        return new User(1, "聂可以", token);
    }

}

1.4 MvcConfig.java(注册拦截器)

import cn.edu.scau.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }

}

1.5 UserContext.java(获取上下文信息的工具类)

import cn.edu.scau.pojo.User;

public class UserContext {
    private static final ThreadLocal<User> userContext = new ThreadLocal<>();

    private UserContext() {
        // Private constructor to prevent instantiation
    }

    public static void setUserContext(User user) {
        UserContext.userContext.set(user);
    }

    public static User getUserContext() {
        return userContext.get();
    }

    public static void clearUserContext() {
        userContext.remove();
    }

} 

1.6 SearchService.java(实现搜索功能)

import cn.edu.scau.common.UserContext;
import cn.edu.scau.pojo.User;
import org.springframework.stereotype.Service;

@Service
public class SearchService {

    private final SaveSearchHistoryService saveSearchHistoryService;

    public SearchService(SaveSearchHistoryService saveSearchHistoryService) {
        this.saveSearchHistoryService = saveSearchHistoryService;
    }

    public void search() {
        // 省略业务代码

        User currentUser = UserContext.getCurrentUser();
        System.err.println("currentUser in search method= " + currentUser);

        // 异步保存搜索记录
        saveSearchHistoryService.saveSearchHistory();
    }

}

1.7 SaveSearchHistoryService.java(实现保存搜索历史功能)

import cn.edu.scau.common.UserContext;
import cn.edu.scau.pojo.User;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class SaveSearchHistoryService {

    @Async("taskExecutor")
    public void saveSearchHistory() {
        // 省略业务代码

        User currentUser = UserContext.getCurrentUser();
        System.out.println("currentUser in saveSearchHistory method= " + currentUser);
    }

}

2. 问题复现

在 SearchService 类中利用多线程异步保存用户的搜索记录时,发现在异步线程中获取不到原线程的上下文对象(也就是 User 对象)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3. 问题产生的原因

问题的关键在于 ThreadLocal,用户信息是存储在 ThreadLocal 中的,而 ThreadLocal 是线程内部的一个独立的变量副本

使用了多线程之后,在其它线程中是获取不到这个变量副本的,所以在异步线程中无法获取到原线程中的上下文信息(也就是 User 对象)

4. 解决方法

4.1 手动传参(不推荐)

最简单的方法就是在异步调用时手动传递 User 参数

在这里插入图片描述

在这里插入图片描述

但是这种方法可能会带来一些潜在的问题:

  1. 代码复杂性增加:随着代码规模的增长,手动传递参数可能会导致代码变得复杂和难以维护。每个异步调用的地方都需要显式地传递 User 参数,这增加了编程的负担
  2. 易出错:由于需要在多个地方传递参数,开发者可能会忘记在某些调用中传递 User 参数,导致程序在运行时出现错误或异常
  3. 上下文丢失:在复杂的异步调用链中,如果某个调用点忘记传递 User 参数,那么后续的调用将无法获取到正确的用户上下文,这可能导致业务逻辑错误
  4. 可扩展性问题:当需要传递的上下文信息增加时,比如除了 User 信息外,还需要传递其他上下文信息,手动传递的方式将变得难以扩展
  5. 代码可读性降低:每个异步方法都需要额外的参数来接收 User 信息,这可能会降低方法的清晰度和可读性

简单点来说,就是如果采用手动传参的方式,随着代码规模的增长,屎山代码就会越来越多

4.2 使用线程装饰器(推荐)

其实线程池在设计的时候就已经考虑到这个问题了,ThreadPoolTaskExecutor 类中有个变量叫做 TaskDecorator,这是线程池的一个装饰器,能在线程任务执行前后添加一些自定义逻辑,以下是 ThreadPoolTaskExecutor 类的部分源码

在这里插入图片描述

4.2.1 自定义线程装饰器

我们可以自定义一个装饰器,让装饰器自动地帮我们传递需要的上下文对象

为了避免内存泄漏,在线程执行完之后,手动调用 UserContext.clearCurrentUser() 方法

在这里插入图片描述

import cn.edu.scau.common.UserContext;
import cn.edu.scau.pojo.User;
import org.springframework.core.task.TaskDecorator;
import org.springframework.lang.NonNull;

public class UserContextDecorator implements TaskDecorator {
    
    @Override
    @NonNull
    public Runnable decorate(@NonNull Runnable runnable) {
        User currentUser = UserContext.getCurrentUser();
        return () -> {
            try {
                UserContext.setCurrentUser(currentUser);
                runnable.run();
            } finally {
                UserContext.clearCurrentUser();
            }
        };
    }

}

4.2.2 设置线程装饰器

在创建线程池时设置线程装饰器

在这里插入图片描述

import cn.edu.scau.decorator.UserContextDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class ThreadPoolConfig {

    /**
     * 核心线程池大小
     */
    private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;

    /**
     * 最大可创建的线程数
     */
    private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 1;

    /**
     * 阻塞队列队列最大长度
     */
    private static final int QUEUE_CAPACITY = 1000;

    /**
     * 线程池中救急线程的存活时间
     */
    private static final int KEEP_ALIVE_SECONDS = 500;

    /**
     * 通过@Bean注解,将任务执行器(TaskExecutor)实例注入到Spring上下文中
     *
     * @return Executor
     */
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        // 创建线程池任务执行器实例
        ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
        // 设置核心线程池大小
        threadPoolExecutor.setCorePoolSize(CORE_POOL_SIZE);
        // 设置最大线程池大小
        threadPoolExecutor.setMaxPoolSize(MAX_POOL_SIZE);
        // 设置队列容量
        threadPoolExecutor.setQueueCapacity(QUEUE_CAPACITY);
        // 设置线程空闲时间
        threadPoolExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
        // 设置拒绝策略为AbortPolicy,当线程池无法接受新任务时抛出异常
        threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 设置线程名称前缀为"task-thread-"
        threadPoolExecutor.setThreadNamePrefix("task-thread-");
        // 设置任务装饰器为UserContextDecorator,用于在执行任务前进行一些预处理
        threadPoolExecutor.setTaskDecorator(new UserContextDecorator());
        // 初始化线程池任务执行器
        threadPoolExecutor.initialize();
        // 返回线程池任务执行器实例
        return threadPoolExecutor;
    }

}

5. ThreadPoolTaskExecutor 与 ThreadPoolExecutor

需要注意的是,ThreadPoolTaskExecutor 是 Spring 提供的线程池,JDK 提供的 ThreadPoolExecutor 并不支持设置线程装饰器,但 Spring 提供的线程池和 JDK 提供的线程池都实现了最顶层的 Executor 接口

在这里插入图片描述


在这里插入图片描述

6. 注意事项

当调用 @Async 注解的方法的类和被调用的方法在同一个类中时,@Async 注解不会生效,因为 Spring 的 AOP 代理是基于整个类的,对于同一个类中的方法调用,不会经过代理,因此 @Async 注解不会被处理(这种现象被称为自调用),与此类似的还有 Spring 中事务失效的问题,具体可以参考我的另一篇博文:Spring中事务失效的常见场景及解决方法


如果项目的启动类中没有启用异步支持,即没有使用 @EnableAsync 注解,那么 @Async 注解同样不会生效


@Async 注解没有指定由 Spring 管理的线程池时,自定义的线程装饰器不会生效

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聂 可 以

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值