程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Spring Boot

    • Spring Boot - 自动配置
    • Spring Boot - 自定义starter
    • Spring Boot - 配置文件
    • Spring Boot - 自定义SpringApplication
    • Spring Boot - 生命周期与事件
    • Spring Boot - 事件驱动
    • Spring Boot - Bean 加载方式
    • Spring Boot - 容器资源感知与获取
    • Spring Boot - 定时任务
    • Spring Boot - 异步任务
      • 1. 异步任务
        • 什么叫异步
      • 2. Java 线程处理异步
      • 3. SpringBoot 异步任务
        • 开启异步任务支持
        • 定义异步方法
        • 获取异步方法的结果
        • @Async注解注意的问题
        • @Async注解引发的循环依赖问题
        • 默认的异步执行行为
        • 为什么高版本没有使用SimpleAsyncTaskExecutor?
        • 自定义异步线程池
        • 应用层级自定义线程池
        • 方法层级自定义线程池
        • 自定义异步异常处理器
    • Spring Boot - 内置日志
    • Spring Boot - 函数式 Web
    • Spring Boot - 响应式远程调用
    • Spring Boot - 接口文档
    • Spring Boot - 单元测试
    • Spring Boot - 内容协商
    • Spring Boot - 参数校验
    • Spring Boot - HTTP客户端工具
    • Spring Boot - 控制器请求映射
    • Spring Boot - 请求参数接收
    • Spring Boot - 通用响应类
    • Spring Boot - 全局异常处理
    • Spring Boot - 整合Druid
    • Spring Boot - 整合Thymeleaf
    • Spring Boot - 国际化实现
    • Spring Boot - 自定义注解
  • Spring高级
  • Spring Boot
scholar
2023-10-30
目录

Spring Boot - 异步任务

# 1. 异步任务

有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。

事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。

# 什么叫异步

异步:如果方法中有休眠任务,不用等任务执行完,直接执行下一个任务

简单来说:客户端发送请求,可以跳过方法,执行下一个方法,如果其中一个A方法有休眠任务,不需要等待,直接执行下一个方法,异步任务(A 方法)会在后台得到执行,等 A 方法的休眠时间到了再去执行 A 方法。

同步:一定要等任务执行完了,得到结果,才执行下一个任务。

# 2. Java 线程处理异步

在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:

@RestController
@RequestMapping("async")
public class AsyncController {
  @GetMapping("/")
  public String index() {
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          // 模拟耗时操作
          Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }).start();
    return "consuming time behavior processing!";
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这时浏览器请求 localhost:8080/async/,就可以很快得到响应,并且耗时任务会在后台得到执行。

一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过 Future 等方式将结果返回,详细内容如下

public class MyReturnableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"线程运行开始");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+"线程运行结束");
        return "result";
    }
}
1
2
3
4
5
6
7
8
9
10
@GetMapping("/task")
public void task() throws ExecutionException, InterruptedException {
  MyReturnableTask myReturnableTask = new MyReturnableTask();
  FutureTask<String> futureTask = new FutureTask<String>(myReturnableTask);
  Thread thread = new Thread(futureTask, "returnableThread");
  thread.start();
  String s = futureTask.get();
  System.out.println(s);
}
1
2
3
4
5
6
7
8
9

事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。

# 3. SpringBoot 异步任务

在Spring Boot应用中,你可以通过使用@EnableAsync和@Async注解轻松实现异步任务。这使得在不阻塞调用线程的情况下执行长时间运行的任务成为可能。下面是关于如何使用这些特性的详细总结:

# 开启异步任务支持

首先,在你的Spring Boot主程序类或配置类上使用@EnableAsync注解。这会告诉Spring框架为那些标记有@Async注解的方法创建代理,使得这些方法可以异步执行。

@SpringBootApplication
@EnableAsync // 开启异步任务支持
public class ApplicationStarter {
    public static void main(String[] args) {
        SpringApplication.run(ApplicationStarter.class, args);
    }
}
1
2
3
4
5
6
7

# 定义异步方法

在你的服务类中,使用@Async注解标记任何你希望异步执行的方法。这些方法可以有任意的参数类型,但它们必须返回void或者Future类型的对象。

@Service
public class AsyncServiceImpl {
    /**
     * 异步执行的无返回值方法。
     * 通过 @Async 注解标记,Spring会在执行此方法时将其放入异步任务中执行。
     * 这意味着主线程调用此方法后不会等待其执行完成,而是立即返回继续执行后续代码。
     */
    @Async
    public void t1() {
        // 模拟耗时任务,例如:数据库操作、文件读写、网络请求等
        try {
            // 模拟任务执行耗时5秒
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
            // 处理中断异常
            e.printStackTrace();
        }
        // 任务完成后的操作
        System.out.println("异步方法中:耗时任务完成");
    }

    /**
     * 异步执行的有返回值方法。
     * 方法返回一个 Future<String> 类型的对象,允许调用者获取异步执行的结果。
     * AsyncResult<String> 是一个 Future 的实现,用于包装异步操作的结果。
     */
    @Async
    public Future<String> t2() {
        // 模拟耗时任务
        try {
            // 模拟任务执行耗时5秒
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
            // 处理中断异常
            e.printStackTrace();
        }
        // 返回异步任务的执行结果
        return new AsyncResult<>("异步任务完成!");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
  1. t1方法:此方法没有返回值。它演示了如何执行一个耗时操作(比如休眠5秒)而不阻塞调用线程。方法执行完成后,会在控制台输出一条消息表示异步任务已完成。
  2. t2方法:此方法返回一个Future<String>对象,允许调用者在未来某个时间点获取异步操作的结果。它同样执行一个耗时操作,然后使用AsyncResult对象返回一个字符串结果。AsyncResult是Spring提供的Future接口的实现,用于从异步方法中返回结果。

# 获取异步方法的结果

当异步方法需要返回结果时,你应该使用Future、ListenableFuture或CompletableFuture类型的返回值。这些返回类型提供了方法来检查异步操作是否完成、等待异步操作的完成,并获取异步操作的结果。

如果你需要在你的控制器或其他服务中获取这些异步方法的结果,你可以调用Future的get()方法。请记住,get()方法是阻塞的,它会等待异步操作完成并返回结果。

@Autowired
private AsyncServiceImpl asyncService;

public void useAsyncMethod() throws Exception {
    Future<String> futureResult = asyncService.t2();
    // 执行其他非阻塞操作...
    String result = futureResult.get(); // 阻塞直到异步方法完成
    System.out.println(result);
}
1
2
3
4
5
6
7
8
9

注意事项

使用@Async标记的方法会在Spring管理的线程池中异步执行。默认情况下,Spring会使用一个简单的线程池,但你可以通过实现AsyncConfigurer接口自定义线程池。

  • 方法的异步执行:使用@Async注解的方法,其调用会立即返回,而方法体内的代码会在另一个线程中执行。
  • 异常处理:异步方法中抛出的未捕获异常默认情况下会被忽略。你可以通过实现AsyncUncaughtExceptionHandler接口来自定义异常处理逻辑。
  • 使用场景:异步方法非常适合处理那些耗时的任务,如发送邮件、调用远程服务等操作,它可以提高程序的响应速度和吞吐量。

# @Async注解注意的问题

异步任务在提高应用性能和响应速度方面非常有用,但它们的使用确实存在一些限制和注意事项。正确理解和遵循这些限制可以帮助您有效地使用@Async注解,避免常见的错误。

  • Public方法:被@Async注解的方法必须是public的。这是因为Spring通过代理模式来实现方法的异步调用,而只有public方法才可以被代理拦截。如果方法不是public的,那么它将不会异步执行,而是像普通方法一样同步执行。

  • 避免同类内调用:在同一个类内部直接调用@Async方法会绕过Spring代理机制,导致异步方法同步执行。这是因为代理是基于类的外部调用实现的。如果需要在同一个类中调用异步方法并保持异步行为,可以考虑将异步方法放在另一个被Spring管理的bean中。

    @Service
    public class MyService {
    
        @Async
        public void asyncMethod() {
            // 异步执行的操作
        }
    
        // syncMethod方法中对asyncMethod的调用实际上是同步的,因为它直接调用了方法,而没有经过Spring创建的代理。
        public void syncMethod() {
            asyncMethod(); // 这个调用实际上是同步的
        }
    }
    // 通过注入自己的bean实例,然后用该实例Bean内部调用@Async注解方法可以解决不走代理的问题,从而使得异步执行生效。
    // 这样操作会产生循环依赖问题,可以使用@Lazy注解可以延迟Bean的初始化,从而避免启动时的循环依赖问题。
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 非Static方法:被@Async注解的方法不能是static的。静态方法不属于任何实例,而Spring的代理模式是基于bean实例的,所以无法代理静态方法。

  • 不能用于生命周期回调:@Async注解不能用于Bean对象的生命周期回调方法(例如,使用@PostConstruct注解的方法)。生命周期回调方法是由容器在特定时点调用的,不适合作为异步任务。

  • Spring管理的Bean:要使用@Async功能,异步方法所在的类必须被Spring管理,即通过@Component、@Service等注解声明为Spring的组件。这样Spring才能创建相应的代理并管理其生命周期。

  • 依赖注入:其他类中使用异步类对象时,必须通过Spring的依赖注入机制(如@Autowired)进行注入,而不能通过手动new的方式创建对象。手动创建的对象不会被Spring上下文管理,因此不会被代理,@Async注解的方法也不会异步执行。

# @Async注解引发的循环依赖问题

当你在Spring中使用@Async注解时,Spring会为这个Bean创建一个代理对象,这样当调用该Bean的方法时,Spring可以捕获这个调用,并在另一个线程中异步执行它。这是通过在Bean初始化的最后阶段添加一个代理来实现的。

循环依赖问题

假设我们有两个Bean,A和B,它们相互依赖。也就是说,A需要B来完成它的初始化,B也需要A来完成它的初始化。

  1. 没有使用@Async时:Spring的三级缓存机制可以解决这种循环依赖,即通过提前暴露一个Bean的创建工厂来允许早期引用,确保A和B都能正确地初始化。但Spring的三级缓存机制本身并不涉及到延迟初始化。
  2. 使用了@Async后:假设A使用了@Async注解。这意味着Spring会在A完全初始化之后,为A创建一个代理对象。但是,如果B在A的代理对象创建之前就尝试注入A,B实际上注入的将是A的原始对象,而不是A的代理对象。因为代理对象是在A初始化的最后阶段创建的。

这里的关键点是:代理对象的创建改变了A的引用。也就是说,一开始B尝试注入的A对象(A的原始对象),和后来Spring管理的A对象(A的代理对象)不一致。这种不一致导致了循环依赖问题的出现。

解决方法

  1. 使用@Lazy注解:在依赖@Async注解的Bean上使用@Lazy注解,可以延迟该Bean的实例化和初始化,这样有助于确保其他依赖项先于该Bean初始化,从而避免循环依赖。
  2. 自定义异步工具类:避免直接使用@Async注解,而是通过自定义异步工具类(使用线程池等)来实现异步逻辑。这种方式不会创建代理对象,因此不会干扰Spring的依赖注入流程。
  3. 避免循环依赖:重新设计Bean的依赖关系,尽量避免让使用@Async注解的Bean参与循环依赖。这可能涉及到重构应用的架构和Bean之间的交互方式。

@Lazy的作用

@Lazy注解使得Bean的初始化被延迟到首次访问时。对于@Async注解的Bean,这意味着其代理对象的创建也被相应地延迟了。如果@Lazy能够确保所有相关的依赖注入在代理对象创建之前完成,那么在代理对象创建之后,相互依赖的Bean之间应该就不会有问题,因为此时它们都将引用正确的代理对象。

# 默认的异步执行行为

当你在方法上使用@Async注解而没有指定任何特定的执行器(Executor)时,Spring会按照以下规则来决定使用哪个线程池执行这些异步任务:

  1. 自动搜索线程池Bean:Spring首先会检查上下文中是否存在唯一的TaskExecutor类型的Bean。如果存在,就使用这个Bean作为异步方法的执行器。
  2. 检查命名为taskExecutor的Bean:如果没有找到唯一的TaskExecutor,Spring接着会尝试查找一个名为taskExecutor的Executor类型的Bean,并使用它。
  3. 回退到SimpleAsyncTaskExecutor:如果以上两个条件都不满足,即没有找到任何合适的TaskExecutor或名为taskExecutor的ExecutorBean,Spring默认会使用SimpleAsyncTaskExecutor执行异步方法。SimpleAsyncTaskExecutor并不重用线程,每次调用都会创建一个新的线程,这在许多业务场景下可能不是最优选择,因为频繁地创建和销毁线程可能会影响性能。

Spring提供了多种线程池:

  • SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。

  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。

  • ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。

  • ThreadPoolTaskScheduler:可以使用cron表达式。

  • ThreadPoolTaskExecutor:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。

下面我们来看SimpleAsyncTaskExecutor提交任务的源码,验证一下这个类是不是真的不重用线程:

@Override
public void execute(Runnable task, long startTimeout) {
    // 确保传入的任务不为空
    Assert.notNull(task, "Runnable must not be null");
    // 如果存在任务装饰器,则对任务进行装饰
    Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
    
    // 检查是否激活了并发节流,并且启动超时大于立即执行的阈值
    if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
        // 在允许执行任务之前进行并发控制
        this.concurrencyThrottle.beforeAccess();
        // 执行经过并发控制处理的任务
        doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
    } else {
        // 如果没有并发节流激活,直接执行任务
        doExecute(taskToUse);
    }
}

protected void doExecute(Runnable task) {
    // 使用线程工厂创建新线程,如果线程工厂不存在,则直接创建新线程
    Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
    // 启动线程,执行任务
    thread.start();
}

public Thread createThread(Runnable runnable) {
    // 创建一个新线程,指定线程组、任务和线程名
    Thread thread = new Thread(getThreadGroup(), runnable, nextThreadName());
    // 设置新线程的优先级
    thread.setPriority(getThreadPriority());
    // 设置新线程是否为守护线程(后台线程)
    thread.setDaemon(isDaemon());
    // 返回创建的线程对象
    return thread;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

doExecute方法:它检查是否有线程工厂threadFactory被设置,如果有,则使用线程工厂创建新线程执行任务;如果没有,则调用createThread(task)方法创建新线程。无论哪种情况,最后都通过调用thread.start()来启动新线程执行传入的任务。

# 为什么高版本没有使用SimpleAsyncTaskExecutor?

写一个简单的demo去验证一下我们的猜想,看看底层是不是真的调用的SimpleAsyncTaskExecutor处理器:

@Service
public class AsyncServiceImpl {
    // 使用 @Async 注解标记的方法 会提交到一个异步任务中进行执行,第一次不会执行该方法
    // 如果不添加该注解,controller 中调用该方法会等待 5 秒在响应
    @Async
    public void t1() {
        // 模拟耗时任务
        try {
           Thread.sleep(TimeUnit.SECONDS.toMillis(5));  // 在这里打上断点
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 因为该异步方法中使用了休眠,所以过 5 秒才会执行下面代码
        System.out.println("异步方法中:耗时时间已走完");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20240401200029674

@Component
public class MyApplicationRunner implements ApplicationRunner {

   @Resource
    private AsyncServiceImpl asyncService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
	asyncService.t1();  // 在SpringBoot应用准备就绪后调用这个异步方法
        System.out.println("ApplicationRunner: 应用就绪,执行自定义逻辑");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

我们在想要观察的异步方法内设置一个断点,在AsyncServiceImpl类的t1方法内部,在Thread.sleep语句或之后设置断点,这样做是为了在异步方法执行时暂停程序,我们有机会查看调用栈。使用ApplicationRunner在项目启动完成后去调用我们的异步方法。当程序执行到断点时,IDE会自动暂停执行,此时我们打开调试视图去查看调用栈:

image-20240401201035149

此时我惊奇的发现底层调用的居然不是我们想要的SimpleAsyncTaskExecutor,而是ThreadPoolTaskExecutor 。

为什么容器中会有 ThreadPoolTaskExecutor 的 Bean 对象,我们并没有去自定义自己的线程池呀?

翻阅了springboot的源码,在spring-boot-autoconfigure 这个包下面的spring.factories中找到了自动配置TaskExecutionAutoConfiguration,在该类中我们看到了如下代码:

@ConditionalOnClass({ThreadPoolTaskExecutor.class})
@AutoConfiguration
@EnableConfigurationProperties({TaskExecutionProperties.class})
public class TaskExecutionAutoConfiguration {
    public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

    // 默认构造函数
    public TaskExecutionAutoConfiguration() {
    }

    // 定义一个Bean来构建TaskExecutor。如果已经存在一个Bean,则不创建新的Bean
    @Bean
    @ConditionalOnMissingBean
    public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties, ObjectProvider<TaskExecutorCustomizer> taskExecutorCustomizers, ObjectProvider<TaskDecorator> taskDecorator) {
        Pool pool = properties.getPool(); // 获取线程池配置属性
        TaskExecutorBuilder builder = new TaskExecutorBuilder(); // 创建TaskExecutor构建器
        
        // 以下是配置线程池属性的代码,包括队列容量、核心线程数、最大线程数等
        builder = builder.queueCapacity(pool.getQueueCapacity());
        builder = builder.corePoolSize(pool.getCoreSize());
        builder = builder.maxPoolSize(pool.getMaxSize());
        builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
        builder = builder.keepAlive(pool.getKeepAlive());
        
        Shutdown shutdown = properties.getShutdown(); // 获取关闭线程池的配置
        builder = builder.awaitTermination(shutdown.isAwaitTermination()); // 是否等待所有任务完成后再关闭线程池
        builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); // 等待终止的时间
        builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); // 设置线程名的前缀
        builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator); // 应用自定义的TaskExecutor定制器
        builder = builder.taskDecorator(taskDecorator.getIfUnique()); // 设置任务装饰器
        return builder;
    }

    // 定义一个线程池TaskExecutor的Bean,名为"applicationTaskExecutor"和"taskExecutor"
    @Lazy // 延迟初始化该Bean,直到首次被引用
    @Bean(
        name = {"applicationTaskExecutor", "taskExecutor"}
    )
    @ConditionalOnMissingBean({Executor.class}) // 仅当不存在Executor类型的Bean时才创建
    public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
        return builder.build(); // 使用TaskExecutorBuilder构建ThreadPoolTaskExecutor
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

此配置类通过TaskExecutionProperties提供的配置来自定义任务执行器,然后点击applicationTaskExecutor方法中的build()方法进入到了TaskExecutorBuilder类,发现默认使用的并不是SimpleAsyncTaskExecutor而是ThreadPoolTaskExecutor。

public ThreadPoolTaskExecutor build() {
    return configure(new ThreadPoolTaskExecutor());
}
1
2
3

由于 TaskExecutionAutoConfiguration是SpringBoot内置的自动配置类,所以SpringBoot 一启动就会扫描它,由此可以断定 ThreadPoolTaskExecutor 是 SpringBoot 项目中 Executor 的默认 Bean 对象。而 @Async 在选择执行器的时候会先去 IOC 容器中先找是否有 TaskExecutor 的 Bean对象,所以在当前版本 SpringBoot 中,@Async 的默认 TaskExecutor 是 ThreadPoolTaskExecutor。

最后查询了各种资料才找到了原因,在spring boot2.1.0.RELEASE版本的时候,新增了TaskExecutionAutoConfiguration配置类,

image-20240401202448454

也就是说新版本,其实spring boot默认使用的已经是ThreadPoolTaskExecutor线程池了,大家不用再去手动更改默认的线程池,我们可以在yml配置文件去配置ThreadPoolTaskExecutor的默认参数了,这样SpringBoot启动的时候就会加载这些参数去实例化ThreadPoolTaskExecutor线程池了。

image-20240401202812024

总结

在 SpringBoot 2.0.9 版本及以前,@Async 默认使用的是 SimpleAsyncTaskExecutor;从 2.1.0 开始,@Async 默认使用的是 ThreadPoolTaskExecutor。这也就是大家的结论里面spring线程池默认不是ThreadPoolTaskExecutor的原因。

那此时我们更改SpringBoot的版本后,再进行一波测试,看看现在是不是调用的SimpleAsyncTaskExecutor处理器:




 


<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.9.RELEASE</version>
</parent>
1
2
3
4
5

image-20240401195724267

最后也成功得到我需要的结果了,在 SpringBoot 2.0.9 版本及以前使用的确实是 SimpleAsyncTaskExecutor处理器;

# 自定义异步线程池

由上我们知道默认情况下,Spring 使用的 Executor 是 SimpleAsyncTaskExecutor,SimpleAsyncTaskExecutor 每次执行任务时启动一个新的线程,它并不是真正的线程池,因为它不重用线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换 SimpleAsyncTaskExecutor。

对于自定义 Executor(自定义线程池),可以分为如下两个层级:

  • 应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的 TaskExecutor 实例或者一个名称为 taskExecutor 的Executor实例即可。
  • 方法层级:即为单独一个或多个方法指定运行线程池,其他未指定的异步方法运行在默认线程池。

# 应用层级自定义线程池

在应用层级自定义线程池,意味着所有未显式指定执行器的@Async注解的方法都会使用这个自定义的线程池执行。你可以通过实现AsyncConfigurer接口并重写getAsyncExecutor方法来实现这一点。

@Configuration
@EnableAsync
public class ExecutorConfig implements AsyncConfigurer {
    /**
     * 定义全局异步线程池。
     * @return 自定义的Executor
     */
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数为CPU核心数
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 设置队列容量
        executor.setQueueCapacity(10);
        // 设置线程名的前缀
        executor.setThreadNamePrefix("App-Level-Async-");
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        // 初始化Executor
        executor.initialize();
        return executor;
    }

    /**
     * 自定义异常处理机制。
     * @return 自定义的异常处理器
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> System.err.println("异常处理:" + ex.getMessage());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 方法层级自定义线程池

如果你想对特定的@Async方法使用不同的线程池,可以通过定义多个执行器(Executor)Bean来实现。然后在@Async注解中通过指定执行器的Bean名称来选择不同的线程池。

@Configuration
public class ExecutorConfig {
  
    /**
     * 定义特定方法的异步线程池。
     * @return 配置好的TaskExecutor
     */
    @Bean("methodLevelExecutor")
    public TaskExecutor methodLevelExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(4);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 设置队列容量
        executor.setQueueCapacity(100);
        // 设置线程名的前缀
        executor.setThreadNamePrefix("Method-Level-Async-");
        // 初始化Executor
        executor.initialize();
        return executor;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在@Async方法上使用自定义线程池:

@Service
public class AsyncService {

    /**
     * 使用指定的线程池执行异步任务。
     * @throws InterruptedException 如果执行被中断
     */
    @Async("methodLevelExecutor")
    public void asyncMethodWithCustomExecutor() throws InterruptedException {
        // 模拟耗时任务
        System.out.println("执行异步任务:" + Thread.currentThread().getName());
        Thread.sleep(5000);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这个案例中,asyncMethodWithCustomExecutor方法会使用名为methodLevelExecutor的线程池执行。如果你在@Async注解中不指定线程池名称,则会使用全局的自定义线程池(如果配置了的话)或Spring的默认线程池(SimpleAsyncTaskExecutor)执行。

通过这种方式,你可以根据不同的业务需求灵活地控制异步任务的执行环境,优化应用的性能和资源利用率。

# 自定义异步异常处理器

在Spring的异步执行框架中,处理异步方法中发生的未捕获异常是一个重要的考虑点。默认情况下,如果异步方法抛出异常,它们可能不会被调用者捕获,因为异步方法的调用立即返回而不是等待异步方法的执行结果。因此,Spring提供了一个机制来全局处理这些未捕获的异步异常,通过实现AsyncConfigurer接口并重写getAsyncUncaughtExceptionHandler方法。

@Configuration
@EnableAsync
public class ExecutorConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        // 省略自定义线程池配置...
    }

    /**
     * 自定义异常处理机制。
     * 这个方法返回一个 AsyncUncaughtExceptionHandler 实例,用于处理异步方法中未捕获的异常。
     * @return 自定义的异常处理器
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        // 返回一个自定义的 AsyncUncaughtExceptionHandler
        return (throwable, method, params) -> {
            // 这里可以自定义异常处理逻辑
            System.err.println("异步任务异常处理:");
            System.err.println("Exception message - " + throwable.getMessage());
            System.err.println("Method name - " + method.getName());
            for (Object param : params) {
                System.err.println("Parameter value - " + param);
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在这个自定义的异常处理器中,getAsyncUncaughtExceptionHandler方法返回了一个AsyncUncaughtExceptionHandler匿名类的实例。这个实例覆盖了handleUncaughtException方法,该方法在异步方法抛出未捕获异常时被调用。

方法参数说明:

  • throwable:抛出的异常对象。
  • method:抛出异常的异步方法。
  • params:异步方法调用时的参数列表。

通过自定义异常处理器,你可以对异步方法中的所有未捕获异常进行集中处理,比如记录日志、发送报警通知等。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
Spring Boot - 定时任务
Spring Boot - 内置日志

← Spring Boot - 定时任务 Spring Boot - 内置日志→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式