44. ThreadPoolTaskExecutor 和 @Async 的使用方法
发布于 • 阅读量 0
44. ThreadPoolTaskExecutor 和 @Async 的使用方法
上一节使用的是 Java 原生线程池:
ThreadPoolExecutor
并且通过:
@Bean
把它交给 Spring 管理。
这种方式没有问题。
不过在 Spring Boot 项目中,还经常使用 Spring 提供的:
ThreadPoolTaskExecutor
它对 Java 原生的 ThreadPoolExecutor 做了一层封装,提供了更符合 Spring 使用习惯的配置方式,并且能够直接配合:
@Async
实现异步方法调用。
Spring 官方文档把 ThreadPoolTaskExecutor 作为常用的任务执行器实现。它底层仍然使用 ThreadPoolExecutor,同时能够参与 Spring 的生命周期管理。
这一节主要完成以下内容:
使用 ThreadPoolTaskExecutor 创建 PDF 线程池;
通过 @EnableAsync 开启异步方法支持;
通过 @Async 指定线程池;
让 PDF 方法在线程池中执行;
理解 @Async 的代理机制;
解决同类内部调用导致 @Async 失效的问题;
处理异步方法中的异常。
ThreadPoolTaskExecutor 是什么
ThreadPoolTaskExecutor 是 Spring 提供的线程池执行器。
它的完整类名是:
org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
它和 Java 原生线程池的关系可以简单理解为:
ThreadPoolExecutor:
Java 提供的真正线程池实现。
ThreadPoolTaskExecutor:
Spring 对 ThreadPoolExecutor 的封装。
使用原生线程池时,需要这样创建:
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
3,
6,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("pdf-worker"),
new ThreadPoolExecutor.AbortPolicy()
);
使用 ThreadPoolTaskExecutor 时,可以改成属性配置:
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(6);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("pdf-worker-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
两种方式表达的核心配置基本一致。
为什么 Spring 项目常用 ThreadPoolTaskExecutor
使用 ThreadPoolTaskExecutor 的主要原因不是它比 ThreadPoolExecutor 更强,而是它和 Spring 结合得更自然。
它可以:
作为 Spring Bean 管理;
参与应用启动和关闭生命周期;
直接配合 @Async 使用;
通过 Bean 名称选择线程池;
配置优雅关闭;
读取当前线程池状态;
在 Spring 配置类中集中维护。
所以在普通 Java 程序中,可以直接使用:
ThreadPoolExecutor
在 Spring Boot 项目中,也可以优先考虑:
ThreadPoolTaskExecutor
但无论使用哪一个,底层线程池的核心概念都没有变化:
核心线程数;
最大线程数;
任务队列;
线程存活时间;
线程工厂;
拒绝策略。
第一步:开启 Spring 异步方法支持
要使用:
@Async
首先要开启 Spring 的异步方法支持。
可以在配置类上添加:
@EnableAsync
例如新建:
com.succos.config.AsyncThreadPoolConfig
代码如下:
package com.succos.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
}
@EnableAsync 的作用是:
开启 Spring 对 @Async 注解的识别和处理。
如果没有添加:
@EnableAsync
即使业务方法写了:
@Async
方法也可能仍然在调用线程中同步执行。
Spring 官方示例同样通过 @EnableAsync 开启异步方法功能。
可以把 @EnableAsync 放在启动类上吗
可以。
例如:
package com.succos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@SpringBootApplication
public class PdfApplication {
public static void main(String[] args) {
SpringApplication.run(
PdfApplication.class,
args
);
}
}
这样也能开启异步方法支持。
不过为了让职责更清楚,我更倾向于把它放在线程池配置类中:
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
}
这样看到配置类时就知道:
这个类负责异步执行相关配置。
第二步:创建 ThreadPoolTaskExecutor Bean
完整配置如下:
package com.succos.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
@Bean(name = "pdfTaskExecutor")
public ThreadPoolTaskExecutor pdfTaskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(6);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix(
"pdf-worker-"
);
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(
true
);
executor.setAwaitTerminationSeconds(30);
return executor;
}
}
这里创建了一个名称为:
pdfTaskExecutor
的线程池 Bean。
后面使用:
@Async("pdfTaskExecutor")
就可以指定 PDF 方法使用这个线程池。
为什么没有调用 initialize
有些示例会在最后写:
executor.initialize();
例如:
@Bean
public ThreadPoolTaskExecutor pdfTaskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.initialize();
return executor;
}
如果 ThreadPoolTaskExecutor 作为 Spring Bean 交给容器管理,Spring 会执行它的初始化生命周期,所以配置方法中通常不需要再手动调用:
initialize()
直接返回配置好的对象即可。
如果是在 Spring 容器之外自己创建和使用 ThreadPoolTaskExecutor,才需要自己关注初始化和关闭。
ThreadPoolTaskExecutor 的核心参数
corePoolSize
executor.setCorePoolSize(3);
表示核心线程数为 3。
可以理解为:
正常情况下,最多先使用 3 个核心线程处理任务。
例如同时提交三个 PDF:
a.pdf;
b.pdf;
c.pdf。
可能分别由:
pdf-worker-1;
pdf-worker-2;
pdf-worker-3。
同时处理。
maxPoolSize
executor.setMaxPoolSize(6);
表示线程池最多可以创建 6 个工作线程。
但不是一提交第 4 个任务,就马上创建第 4 个线程。
在线程池使用有界队列时,通常是:
先创建核心线程;
核心线程都忙以后,任务进入队列;
队列装满以后,才继续创建非核心线程;
线程数达到最大线程数以后,再提交任务会触发拒绝策略。
当前配置是:
核心线程数:3;
最大线程数:6;
队列容量:100。
因此大致执行顺序是:
前 3 个任务由核心线程执行;
接下来的任务进入长度为 100 的队列;
队列满了以后,才可能把线程数从 3 增加到 6;
线程达到 6,并且队列也满了以后,触发拒绝策略。
所以:
maxPoolSize
是否容易发挥作用,和:
queueCapacity
有很大关系。
queueCapacity
executor.setQueueCapacity(100);
表示任务队列最多保存 100 个等待执行的任务。
这个队列控制的是:
当前线程都在忙时,还允许多少个任务排队。
队列不能盲目设置得特别大。
例如:
executor.setQueueCapacity(100000);
虽然能减少任务被拒绝的概率,但可能导致:
任务大量积压;
内存占用增加;
用户等待时间越来越长;
系统已经过载,但外部看不出来;
应用关闭时还有大量任务没有处理。
队列容量应该代表系统愿意承受的待处理任务数量。
keepAliveSeconds
executor.setKeepAliveSeconds(60);
表示超过核心线程数的非核心线程,在空闲一定时间后可以被回收。
例如线程池因为高峰期从 3 个线程扩展到 6 个线程。
高峰过去以后,多出来的 3 个非核心线程空闲超过 60 秒,就可以被销毁。
核心线程默认通常不会因为空闲而被回收。
如果希望核心线程也可以超时回收,可以设置:
executor.setAllowCoreThreadTimeOut(true);
但对于长期运行、持续接收 PDF 任务的应用,一般不需要急着开启。
threadNamePrefix
executor.setThreadNamePrefix(
"pdf-worker-"
);
设置线程名称前缀。
最终日志中可能出现:
pdf-worker-1
pdf-worker-2
pdf-worker-3
线程名称是排查异步问题的重要信息。
如果不设置,日志中可能只看到不够直观的默认线程名。
设置业务名称以后,可以快速判断:
PDF 任务是否进入了正确线程池;
@Async 是否真的生效;
任务有没有误跑到公共线程池;
不同业务线程池之间是否相互混用。
rejectedExecutionHandler
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
表示线程池无法继续接收任务时,抛出拒绝异常。
常见拒绝策略包括:
AbortPolicy:
直接抛出 RejectedExecutionException。
CallerRunsPolicy:
由提交任务的线程执行任务。
DiscardPolicy:
直接丢弃任务,不抛异常。
DiscardOldestPolicy:
丢弃队列中等待最久的任务,再尝试提交新任务。
PDF 处理属于明确的业务任务。
如果任务提交失败,我希望能够明确知道,而不是悄悄丢失任务。
所以更适合使用:
AbortPolicy
然后在接口层或任务提交层处理拒绝异常。
设置应用关闭时的处理方式
配置中写了:
executor.setWaitForTasksToCompleteOnShutdown(
true
);
以及:
executor.setAwaitTerminationSeconds(30);
表示应用关闭时,希望给正在执行或排队的任务一定时间完成。
setAwaitTerminationSeconds(30) 表示 Spring 容器关闭过程中,最多等待线程池终止 30 秒。Spring 官方文档说明,这个等待时间可以让剩余任务继续使用其他仍由容器管理的资源。
但要注意:
等待 30 秒不代表所有 PDF 一定能完成;
超过等待时间后,应用仍然需要继续关闭;
长任务仍然应该通过任务状态和恢复机制管理。
对于一个真正的 PDF 后台任务系统,不能只依赖应用关闭时等待几十秒。
还需要考虑:
任务状态保存;
应用重启恢复;
临时文件清理;
重复任务处理;
任务幂等。
第三步:编写带 @Async 的 PDF Service
新建:
com.succos.service.PdfAsyncService
代码如下:
package com.succos.service;
import com.succos.dto.PdfTaskResult;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.concurrent.CompletableFuture;
@Service
public class PdfAsyncService {
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
long start =
System.currentTimeMillis();
try {
String targetPath =
processPdf(file);
long cost =
System.currentTimeMillis()
- start;
PdfTaskResult result =
PdfTaskResult.success(
file.getName(),
targetPath,
cost
);
return CompletableFuture
.completedFuture(result);
} catch (Exception e) {
long cost =
System.currentTimeMillis()
- start;
PdfTaskResult result =
PdfTaskResult.fail(
file.getName(),
getErrorMessage(e),
cost
);
return CompletableFuture
.completedFuture(result);
}
}
private String processPdf(File file) {
validateFile(file);
System.out.println(
Thread.currentThread().getName()
+ " 开始处理:"
+ file.getName()
);
sleep(3000);
String targetPath =
"output/"
+ file.getName()
.replace(
".pdf",
"-watermark.pdf"
);
System.out.println(
Thread.currentThread().getName()
+ " 处理完成:"
+ targetPath
);
return targetPath;
}
private void validateFile(File file) {
if (file == null) {
throw new IllegalArgumentException(
"PDF 文件不能为空"
);
}
if (!file.exists()) {
throw new IllegalArgumentException(
"PDF 文件不存在"
);
}
if (!file.isFile()) {
throw new IllegalArgumentException(
"PDF 路径不是文件"
);
}
if (!file.canRead()) {
throw new IllegalArgumentException(
"PDF 文件不可读取"
);
}
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(
"PDF 处理任务被中断",
e
);
}
}
private String getErrorMessage(
Throwable throwable
) {
Throwable cause = throwable;
while (cause.getCause() != null) {
cause = cause.getCause();
}
String message =
cause.getMessage();
if (message == null
|| message.isBlank()) {
return cause
.getClass()
.getSimpleName();
}
return message;
}
}
这里最关键的是:
@Async("pdfTaskExecutor")
它表示:
这个方法通过 Spring 异步机制执行;
使用名称为 pdfTaskExecutor 的线程池。
@Async("pdfTaskExecutor") 中的名称是什么
这里写的:
@Async("pdfTaskExecutor")
对应配置类中的 Bean 名称:
@Bean(name = "pdfTaskExecutor")
两个名称必须一致。
如果写成:
@Async("pdfExecutor")
但容器中只有:
pdfTaskExecutor
Spring 就找不到指定执行器。
所以我会保持名称统一:
@Bean(name = "pdfTaskExecutor")
对应:
@Async("pdfTaskExecutor")
调用 @Async 方法时发生了什么
假设另一个 Service 调用:
CompletableFuture<PdfTaskResult> future =
pdfAsyncService.processPdfAsync(file);
大致流程是:
1. 调用方调用 PdfAsyncService;
2. 实际调用先经过 Spring 生成的代理对象;
3. 代理发现方法上有 @Async;
4. 代理把方法任务提交到 pdfTaskExecutor;
5. 调用方快速拿到 CompletableFuture;
6. PDF 方法在 pdf-worker 线程中执行;
7. 方法完成后,CompletableFuture 得到结果。
所以 @Async 并不是 Java 语言本身的功能。
它依赖 Spring 的代理机制。
为什么方法里返回 completedFuture
方法的返回类型是:
CompletableFuture<PdfTaskResult>
但方法体已经由:
@Async
安排到线程池中执行了。
因此,在方法内部完成业务处理后,可以直接返回:
CompletableFuture.completedFuture(result)
例如:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
PdfTaskResult result =
processPdf(file);
return CompletableFuture
.completedFuture(result);
}
这里的 completedFuture() 表示:
当前异步方法的业务已经执行结束;
把结果包装成一个已经完成的 CompletableFuture。
不要在 @Async 方法中重复 supplyAsync
下面这种写法通常没有必要:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
return CompletableFuture.supplyAsync(
() -> processPdf(file),
pdfTaskExecutor
);
}
因为:
@Async 已经把方法放进了线程池;
方法内部又通过 supplyAsync 提交了一次任务。
这相当于:
第一次异步:调用 processPdfAsync;
第二次异步:执行 processPdf。
如果没有特殊需求,这只是重复调度。
更直接的写法是:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
PdfTaskResult result =
processPdf(file);
return CompletableFuture
.completedFuture(result);
}
或者不使用 @Async,改为手动编排:
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
return CompletableFuture.supplyAsync(
() -> processPdf(file),
pdfTaskExecutor
);
}
两种方案选择一种即可。
@Async 和 supplyAsync 怎么选
使用 @Async
适合:
希望通过注解声明异步方法;
方法本身就是一个完整异步业务边界;
不需要在方法内部手动选择复杂执行流程;
团队已经统一使用 Spring 异步机制。
代码形式:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
PdfTaskResult result =
processPdf(file);
return CompletableFuture
.completedFuture(result);
}
使用 CompletableFuture.supplyAsync
适合:
需要在一个方法中编排多个异步步骤;
需要动态选择执行器;
需要 thenCompose、thenCombine、allOf;
希望异步行为在代码中更加显式;
不希望依赖 Spring 代理调用。
代码形式:
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
return CompletableFuture.supplyAsync(
() -> processPdf(file),
pdfTaskExecutor
);
}
两种方式不是必须一起使用
我现在会这样判断:
单个完整业务方法异步化:
可以使用 @Async。
复杂 CompletableFuture 流程编排:
使用 supplyAsync、thenCompose、thenCombine 等方法。
不要认为:
@Async + supplyAsync
叠加以后会更异步。
大多数情况下只会增加一次不必要的任务提交。
第四步:调用异步 Service
新建一个任务调度 Service:
com.succos.service.PdfTaskService
代码如下:
package com.succos.service;
import com.succos.dto.PdfTaskResult;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.concurrent.CompletableFuture;
@Service
public class PdfTaskService {
private final PdfAsyncService pdfAsyncService;
public PdfTaskService(
PdfAsyncService pdfAsyncService
) {
this.pdfAsyncService =
pdfAsyncService;
}
public CompletableFuture<PdfTaskResult>
submitPdf(String fileName) {
File file =
new File(
"input",
fileName
);
return pdfAsyncService
.processPdfAsync(file);
}
}
这里要注意:
PdfTaskService 调用 PdfAsyncService;
是两个不同的 Spring Bean。
这一点和 @Async 的代理机制有关。
第五步:Controller 返回 CompletableFuture
Controller 可以这样写:
package com.succos.controller;
import com.succos.dto.PdfTaskResult;
import com.succos.service.PdfTaskService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/pdf")
public class PdfTaskController {
private final PdfTaskService pdfTaskService;
public PdfTaskController(
PdfTaskService pdfTaskService
) {
this.pdfTaskService =
pdfTaskService;
}
@PostMapping("/watermark")
public CompletableFuture<PdfTaskResult>
addWatermark(
@RequestParam String fileName
) {
return pdfTaskService
.submitPdf(fileName);
}
}
请求:
POST /pdf/watermark?fileName=test.pdf
日志可能显示:
pdf-worker-1 开始处理:test.pdf
pdf-worker-1 处理完成:output/test-watermark.pdf
这说明:
Controller 请求线程没有直接执行 PDF 任务;
PDF 任务进入了 pdfTaskExecutor。
@Async 最大的坑:同类内部调用会失效
这是使用 @Async 时最重要的问题之一。
假设这样写:
@Service
public class PdfTaskService {
public void submitPdf(File file) {
processPdfAsync(file);
}
@Async("pdfTaskExecutor")
public void processPdfAsync(File file) {
System.out.println(
Thread.currentThread().getName()
);
processPdf(file);
}
private void processPdf(File file) {
// 处理 PDF
}
}
在同一个类中:
submitPdf()
直接调用:
processPdfAsync()
这时 @Async 很可能不会生效。
方法可能仍然在原来的调用线程中执行。
为什么同类内部调用会失效
Spring 默认通过代理对象处理 @Async。
外部调用时:
其他 Bean
↓
Spring 代理对象
↓
发现 @Async
↓
提交线程池
↓
真正的业务方法
但同类内部调用相当于:
this.processPdfAsync(file);
调用没有经过 Spring 代理对象。
流程变成:
当前对象内部直接调用方法
↓
没有经过代理
↓
@Async 拦截逻辑没有执行
↓
同步执行
Spring 官方文档明确说明,默认的 @Async 处理模式基于代理,只有通过代理的调用才能被拦截;同一个类内部的本地调用无法被这种方式拦截。
正确做法:把异步方法拆到另一个 Bean
推荐结构是:
PdfTaskService:
负责业务调度。
PdfAsyncService:
负责异步执行。
例如:
@Service
public class PdfTaskService {
private final PdfAsyncService pdfAsyncService;
public PdfTaskService(
PdfAsyncService pdfAsyncService
) {
this.pdfAsyncService =
pdfAsyncService;
}
public CompletableFuture<PdfTaskResult>
submitPdf(File file) {
return pdfAsyncService
.processPdfAsync(file);
}
}
异步类:
@Service
public class PdfAsyncService {
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
PdfTaskResult result =
processPdf(file);
return CompletableFuture
.completedFuture(result);
}
}
因为调用发生在两个 Spring Bean 之间,所以会经过代理。
这是最清楚、也比较容易维护的方式。
不建议通过自己注入自己解决
有时会看到这种写法:
@Service
public class PdfTaskService {
private final PdfTaskService self;
public PdfTaskService(
PdfTaskService self
) {
this.self = self;
}
public void submitPdf(File file) {
self.processPdfAsync(file);
}
@Async
public void processPdfAsync(File file) {
// 处理 PDF
}
}
这种做法可能引入:
循环依赖;
代码难以理解;
代理对象和真实对象混淆;
测试困难。
也有人通过:
AopContext.currentProxy()
拿当前代理对象。
这会让业务代码和 Spring AOP 机制高度耦合。
所以更推荐:
把异步方法拆到独立 Service。
private 方法上的 @Async 通常不会达到预期
例如:
@Async("pdfTaskExecutor")
private void processPdfAsync(File file) {
}
然后在类内部调用:
processPdfAsync(file);
这种调用不会经过外部代理,因此通常不能实现预期的异步效果。
更合理的写法是:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
}
并且由其他 Spring Bean 调用。
我会记住两个条件:
异步方法定义在 Spring Bean 中;
异步方法由另一个 Bean 通过代理调用。
@Async 方法可以返回什么
常见返回类型包括:
void;
Future<T>;
CompletableFuture<T>。
对于 PDF 任务,我更推荐:
CompletableFuture<PdfTaskResult>
因为调用方可以:
获取处理结果;
继续 thenApply;
继续 thenCompose;
增加 exceptionally;
等待多个任务;
统一收集结果。
例如:
CompletableFuture<PdfTaskResult> future =
pdfAsyncService
.processPdfAsync(file);
future.thenAccept(result -> {
System.out.println(
result.getMessage()
);
});
尽量少使用 void 返回异步方法
下面这种写法可以执行:
@Async("pdfTaskExecutor")
public void processPdfAsync(File file) {
processPdf(file);
}
但是调用方拿不到:
任务结果;
任务异常;
任务完成状态;
任务返回值。
如果方法抛出异常,也不能通过返回的 Future 获取。
Spring 为 void 类型的异步方法提供了:
AsyncUncaughtExceptionHandler
用来处理无法通过 Future 返回给调用方的未捕获异常。
因此,重要业务任务更适合返回:
CompletableFuture<PdfTaskResult>
而不是 void。
CompletableFuture 返回值怎么处理异常
异步方法可以直接让异常抛出:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
String targetPath =
processPdf(file);
PdfTaskResult result =
PdfTaskResult.success(
file.getName(),
targetPath,
0
);
return CompletableFuture
.completedFuture(result);
}
如果 processPdf(file) 抛出异常,Spring 返回的 CompletableFuture 会异常完成。
调用方可以处理:
return pdfAsyncService
.processPdfAsync(file)
.exceptionally(ex -> {
return PdfTaskResult.fail(
file.getName(),
getErrorMessage(ex),
0
);
});
这样异常处理可以放在调用链中。
异常处理放在异步方法内部还是外部
放在异步方法内部
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
try {
String path =
processPdf(file);
return CompletableFuture.completedFuture(
PdfTaskResult.success(
file.getName(),
path,
0
)
);
} catch (Exception e) {
return CompletableFuture.completedFuture(
PdfTaskResult.fail(
file.getName(),
e.getMessage(),
0
)
);
}
}
优点是:
方法最终总能返回 PdfTaskResult;
批量任务比较容易收集结果。
缺点是:
异常已经被转成普通结果;
调用方不能再通过异常链统一处理。
放在调用链外部
异步方法只负责执行:
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
String path =
processPdf(file);
return CompletableFuture.completedFuture(
PdfTaskResult.success(
file.getName(),
path,
0
)
);
}
调用方统一处理:
return pdfAsyncService
.processPdfAsync(file)
.exceptionally(ex -> {
return PdfTaskResult.fail(
file.getName(),
getErrorMessage(ex),
0
);
});
优点是:
异步方法职责简单;
异常处理策略可以由不同调用方决定。
对于批量 PDF 项目,我更倾向于最终把异常转换成统一的:
PdfTaskResult
具体放在异步 Service 还是调度 Service,要根据项目职责划分决定。
void 异步方法怎么处理异常
假设确实有一个不需要返回值的异步方法:
@Async("pdfTaskExecutor")
public void clearTemporaryFile(File file) {
throw new RuntimeException(
"临时文件删除失败"
);
}
由于返回类型是:
void
调用方拿不到 CompletableFuture。
这时可以实现:
AsyncConfigurer
配置全局异步异常处理器。
例如:
package com.succos.config;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import java.lang.reflect.Method;
import java.util.Arrays;
@Configuration
@EnableAsync
public class AsyncExceptionConfig
implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler
getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(
Throwable ex,
Method method,
Object... params
) {
System.err.println(
"异步方法执行异常"
);
System.err.println(
"方法名:"
+ method.getName()
);
System.err.println(
"参数:"
+ Arrays.toString(params)
);
ex.printStackTrace();
}
};
}
}
这样 void 异步方法中未捕获的异常,可以由这个处理器记录。
不过对于关键 PDF 任务,仍然推荐返回:
CompletableFuture<PdfTaskResult>
这样异常、结果和状态都更容易管理。
@Async 默认使用哪个线程池
如果只写:
@Async
public void processPdfAsync(File file) {
}
没有指定:
@Async("pdfTaskExecutor")
Spring 会尝试选择默认异步执行器。
但当项目中存在多个线程池时,依赖默认选择容易造成混乱。
例如系统里有:
pdfTaskExecutor;
uploadTaskExecutor;
notifyTaskExecutor。
为了让代码更明确,我会直接写:
@Async("pdfTaskExecutor")
这样只看方法注解,就能知道它使用哪个线程池。
多个业务线程池的配置
完整配置可以写成:
package com.succos.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig {
@Bean(name = "pdfTaskExecutor")
public ThreadPoolTaskExecutor
pdfTaskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(6);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix(
"pdf-worker-"
);
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(
true
);
executor.setAwaitTerminationSeconds(30);
return executor;
}
@Bean(name = "uploadTaskExecutor")
public ThreadPoolTaskExecutor
uploadTaskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix(
"upload-worker-"
);
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(
true
);
executor.setAwaitTerminationSeconds(30);
return executor;
}
@Bean(name = "notifyTaskExecutor")
public ThreadPoolTaskExecutor
notifyTaskExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix(
"notify-worker-"
);
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.AbortPolicy()
);
executor.setWaitForTasksToCompleteOnShutdown(
true
);
executor.setAwaitTerminationSeconds(30);
return executor;
}
}
对应的业务方法:
@Async("pdfTaskExecutor")
public CompletableFuture<String>
processPdfAsync(File file) {
}
@Async("uploadTaskExecutor")
public CompletableFuture<String>
uploadAsync(String filePath) {
}
@Async("notifyTaskExecutor")
public CompletableFuture<Void>
notifyAsync(String userId) {
}
这样可以实现业务隔离。
@Async 和事务的关系
假设调用方法上有事务:
@Transactional
public void createTask() {
saveTask();
pdfAsyncService.processPdfAsync(file);
}
异步方法会在线程池中的另一个线程执行。
线程切换以后,原调用线程中的事务上下文通常不会自动跟着传过去。
因此不能简单认为:
外层事务回滚;
异步方法中的数据库操作也一定跟着回滚。
异步方法如果需要事务,应该根据业务需要在异步方法内部建立自己的事务边界。
例如:
@Async("pdfTaskExecutor")
@Transactional
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
// 独立异步事务
}
但还要考虑一个现实问题:
外层事务可能还没有提交;
异步线程就已经开始查询数据。
异步线程可能查询不到外层刚保存但尚未提交的数据。
因此,事务内启动异步任务时,要谨慎设计调用时机。
比较稳妥的方式可能是:
事务提交后再发布事件;
监听事务提交后的事件;
再启动异步任务。
这部分属于异步任务和事务协作的内容,后面可以单独展开。
@Async 不会让业务自动变快
给方法加上:
@Async
只是改变方法在哪个线程执行。
它并不会自动减少:
PDF 实际处理时间;
文件读写时间;
数据库执行时间;
网络上传时间。
例如一个 PDF 仍然需要 10 秒处理。
使用 @Async 后,它可能仍然需要 10 秒。
改变的是:
调用线程不必亲自执行这 10 秒任务;
任务交给线程池执行。
如果同时提交多个互不依赖的 PDF,线程池可以让它们并发处理,从而缩短整批任务总时间。
但线程数过多也可能导致:
CPU 争用;
内存压力;
磁盘 IO 争用;
上下文切换增加。
所以异步不是线程越多越好。
ThreadPoolTaskExecutor 获取运行状态
如果需要监控线程池,可以注入:
ThreadPoolTaskExecutor
例如:
package com.succos.service;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
@Service
public class PdfExecutorMonitorService {
private final ThreadPoolTaskExecutor
pdfTaskExecutor;
public PdfExecutorMonitorService(
@Qualifier("pdfTaskExecutor")
ThreadPoolTaskExecutor pdfTaskExecutor
) {
this.pdfTaskExecutor =
pdfTaskExecutor;
}
public void printStatus() {
int poolSize =
pdfTaskExecutor.getPoolSize();
int activeCount =
pdfTaskExecutor
.getActiveCount();
int queueSize =
pdfTaskExecutor
.getThreadPoolExecutor()
.getQueue()
.size();
long completedTaskCount =
pdfTaskExecutor
.getThreadPoolExecutor()
.getCompletedTaskCount();
System.out.println(
"当前线程数:"
+ poolSize
);
System.out.println(
"活跃线程数:"
+ activeCount
);
System.out.println(
"队列任务数:"
+ queueSize
);
System.out.println(
"已完成任务数:"
+ completedTaskCount
);
}
}
可以重点监控:
当前线程数;
活跃线程数;
任务队列长度;
已完成任务数;
任务拒绝次数。
如果长期出现:
活跃线程数达到最大值;
队列接近满载;
任务持续被拒绝。
说明线程池已经接近或超过当前承载能力。
使用 @Async 处理批量 PDF
异步 Service:
@Service
public class PdfAsyncService {
@Async("pdfTaskExecutor")
public CompletableFuture<PdfTaskResult>
processPdfAsync(File file) {
long start =
System.currentTimeMillis();
try {
String path =
processPdf(file);
long cost =
System.currentTimeMillis()
- start;
return CompletableFuture
.completedFuture(
PdfTaskResult.success(
file.getName(),
path,
cost
)
);
} catch (Exception e) {
long cost =
System.currentTimeMillis()
- start;
return CompletableFuture
.completedFuture(
PdfTaskResult.fail(
file.getName(),
e.getMessage(),
cost
)
);
}
}
}
批量调度 Service:
@Service
public class PdfBatchTaskService {
private final PdfAsyncService
pdfAsyncService;
public PdfBatchTaskService(
PdfAsyncService pdfAsyncService
) {
this.pdfAsyncService =
pdfAsyncService;
}
public CompletableFuture<List<PdfTaskResult>>
processBatch(List<File> fileList) {
List<CompletableFuture<PdfTaskResult>>
futureList = fileList
.stream()
.map(
pdfAsyncService
::processPdfAsync
)
.toList();
CompletableFuture<Void> allFuture =
CompletableFuture.allOf(
futureList.toArray(
new CompletableFuture[0]
)
);
return allFuture.thenApply(
unused -> futureList
.stream()
.map(
CompletableFuture::join
)
.toList()
);
}
}
这里:
每个 PDF 调用一次 @Async 方法;
每个调用返回 CompletableFuture;
所有任务进入同一个 pdfTaskExecutor;
allOf 等待所有 PDF 完成;
最后收集全部结果。
并发数量仍然由:
pdfTaskExecutor
控制。
不是由:
allOf
控制。
@Async 任务被拒绝时怎么办
线程池使用:
new ThreadPoolExecutor.AbortPolicy()
当线程池和队列都满时,任务提交可能抛出:
TaskRejectedException
或者底层的:
RejectedExecutionException
这类异常发生在任务提交阶段。
因此,调用异步方法时也要考虑任务是否成功提交。
例如可以在业务入口统一捕获并返回:
当前任务过多,请稍后重试。
也可以增加全局异常处理。
但不要简单改成:
DiscardPolicy
否则任务可能被直接丢弃,而业务系统没有任何记录。
对于 PDF 这类用户明确提交的任务,我更倾向于:
拒绝也要记录;
拒绝也要返回明确结果;
不能静默丢弃。
ThreadPoolTaskExecutor 和 ThreadPoolExecutor 怎么选
两者都能完成 PDF 任务。
更适合 ThreadPoolExecutor 的情况
普通 Java 项目;
不依赖 Spring;
需要完全使用 JDK 原生接口;
希望直接控制底层 ThreadPoolExecutor。
更适合 ThreadPoolTaskExecutor 的情况
Spring Boot 项目;
需要配合 @Async;
需要 Spring 管理生命周期;
需要统一配置多个任务执行器;
希望方便读取线程池状态。
在当前 Spring Boot PDF 项目中,我更倾向于使用:
ThreadPoolTaskExecutor
@Async 和 CompletableFuture 手动编排怎么选
简单异步业务边界
例如:
提交一个 PDF;
后台处理;
返回一个 CompletableFuture。
可以使用:
@Async("pdfTaskExecutor")
代码简单直观。
复杂异步任务链
例如:
添加水印;
上传文件;
查询用户;
合并用户和文件结果;
保存数据库;
发送通知。
更适合显式使用:
CompletableFuture.supplyAsync();
thenCompose();
thenCombine();
thenApply();
exceptionally();
因为流程关系更清楚。
常见错误总结
错误一:忘记添加 @EnableAsync
@Async
public void processPdf() {
}
但项目中没有:
@EnableAsync
异步注解不会按预期工作。
错误二:同类内部调用
public void submit() {
processAsync();
}
@Async
public void processAsync() {
}
同类内部调用没有经过 Spring 代理,异步可能失效。
错误三:异步类不是 Spring Bean
PdfAsyncService service =
new PdfAsyncService();
service.processPdfAsync(file);
自己 new 出来的对象不受 Spring 代理管理。
@Async 不会生效。
应该注入:
private final PdfAsyncService
pdfAsyncService;
错误四:@Async 中又重复 supplyAsync
@Async
public CompletableFuture<String>
processAsync() {
return CompletableFuture.supplyAsync(
() -> process()
);
}
没有特殊目的时,这是重复异步调度。
错误五:不指定业务线程池
@Async
public void processPdf() {
}
当项目存在多个线程池时,不明确指定执行器,容易造成线程池使用混乱。
更清楚的是:
@Async("pdfTaskExecutor")
错误六:关键任务使用 void 返回
@Async
public void processPdf() {
}
调用方无法方便获取任务结果和异常。
关键任务更适合返回:
CompletableFuture<PdfTaskResult>
错误七:把 @Async 当成后台任务系统
@Async 只是把方法提交到线程池。
它不会自动提供:
任务 ID;
任务持久化;
任务进度;
失败重试;
应用重启恢复;
分布式任务调度。
如果 PDF 任务需要执行几分钟,并且要求可靠完成,就不能只靠:
@Async
还需要完整的任务状态管理。
最终代码结构
这一节完成后,项目结构可以是:
src/main/java/com/succos
├── PdfApplication.java
├── config
│ └── AsyncThreadPoolConfig.java
├── controller
│ └── PdfTaskController.java
├── dto
│ └── PdfTaskResult.java
└── service
├── PdfTaskService.java
├── PdfAsyncService.java
└── PdfBatchTaskService.java
各类职责如下。
AsyncThreadPoolConfig
开启 @Async;
创建 ThreadPoolTaskExecutor;
设置线程数;
设置队列;
设置拒绝策略;
设置关闭方式。
PdfAsyncService
定义真正的 @Async 方法;
执行单个 PDF 处理;
返回 CompletableFuture。
PdfTaskService
负责普通任务提交;
调用独立的异步 Service;
避免同类内部调用。
PdfBatchTaskService
提交多个 PDF;
保存 CompletableFuture 列表;
通过 allOf 等待任务;
收集批量结果。
PdfTaskController
接收 HTTP 请求;
调用任务 Service;
返回异步结果。
这一节小结
这一节我主要记住几点:
1. ThreadPoolTaskExecutor 是 Spring 对 ThreadPoolExecutor 的封装;
2. 使用 @Async 前需要通过 @EnableAsync 开启异步支持;
3. @Async("pdfTaskExecutor") 可以指定具体业务线程池;
4. ThreadPoolTaskExecutor 可以由 Spring 管理初始化和销毁;
5. @Async 已经完成线程切换,方法内部通常不需要再重复 supplyAsync;
6. @Async 默认依赖 Spring 代理,同类内部调用不会经过代理;
7. 异步方法最好拆到独立的 Spring Service 中;
8. 关键业务任务更适合返回 CompletableFuture,而不是 void;
9. void 异步方法的未捕获异常可以通过 AsyncUncaughtExceptionHandler 处理;
10. @Async 只是异步执行工具,不是完整的可靠任务系统。
用一句话总结:
ThreadPoolTaskExecutor 负责提供 Spring 线程池,@Async 负责把经过代理的方法提交到指定线程池中执行。
下一节继续学习:
为什么 @Async 同类调用会失效;
Spring 代理对象到底是什么;
为什么从 Controller 调用可以异步,从本类调用却不行。
这会把前面学习过的 Spring AOP、代理对象和 @Async 串起来。