定位:给 Java 新手看的线程池入门博客。读完以后,你应该能理解线程池为什么存在、核心参数怎么配置、Spring Boot 3.x 项目里怎么用,以及常见坑怎么避开。
参考资料
本文参考了官方文档和主流技术资料:
- Oracle JDK 17
ThreadPoolExecutor官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html - Oracle JDK 17
ExecutorService官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ExecutorService.html - Oracle JDK 17
Executors官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Executors.html - Oracle JDK 17
CompletableFuture官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/CompletableFuture.html - Spring Boot System Requirements 官方文档:https://docs.spring.io/spring-boot/system-requirements.html
- Spring Framework Task Execution and Scheduling 官方文档:https://docs.spring.io/spring-framework/reference/integration/scheduling.html
- Spring
ThreadPoolTaskExecutor官方 Javadoc:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html - Alibaba Java Coding Guidelines:https://github.com/alibaba/Alibaba-Java-Coding-Guidelines
- Baeldung ExecutorService Guide:https://www.baeldung.com/java-executor-service-tutorial
文中的代码示例均为通用示例,用于说明线程池设计思路,不依赖任何特定业务系统。
本文中的 Spring 3.x 指 Spring Boot 3.x。Spring Boot 3.x 基于 Spring Framework 6.x,并要求 Java 17 或更高版本。本文选择 JDK 17,是因为它仍是很多企业项目的长期支持版本;虚拟线程属于 JDK 21 之后更常见的选择,本文不把它作为主线,以免把"线程池入门"讲得过早复杂化。
一、为什么要用线程池?
如果每来一个任务就 new Thread(),看起来很直观:
java
new Thread(() -> doSomething()).start();
但在真实后端服务里,这种写法有几个明显问题:
- 线程创建和销毁有成本,任务多时会浪费 CPU 和内存。
- 线程数量不受控制,流量高峰时可能把机器打满。
- 缺少统一的队列、拒绝策略、异常处理和监控指标。
- 线程名不清晰,线上排查问题时很难定位是哪类任务。
线程池的价值可以一句话概括:把"无限制创建线程"变成"有限资源内有序处理任务"。
它解决的是两个问题:
- 复用线程,降低频繁创建线程的成本。
- 限制并发量和排队量,保护应用和下游资源。
二、先认识线程池的核心接口
Java 线程池常见接口和类如下:
| 名称 | 作用 |
|---|---|
Executor |
最基础的任务执行接口,只有 execute(Runnable) |
ExecutorService |
在 Executor 基础上增加生命周期管理、submit、Future 等能力 |
ThreadPoolExecutor |
最常用的线程池实现类,可配置核心线程数、最大线程数、队列、拒绝策略 |
ScheduledExecutorService |
支持延迟任务、周期任务 |
ForkJoinPool |
面向分治任务和并行计算,也常被 CompletableFuture 默认异步方法使用 |
ThreadPoolTaskExecutor |
Spring 对 ThreadPoolExecutor 的封装,适合 Spring Boot 项目通过 Bean 管理 |
新手可以先记住两句话:
- 普通 Java 项目里,核心是
ThreadPoolExecutor。 - Spring Boot 项目里,优先把
ThreadPoolTaskExecutor配成 Bean,再通过@Async或CompletableFuture使用。
三、ThreadPoolExecutor 的 7 个核心参数
ThreadPoolExecutor 最完整的构造方法如下:
java
ExecutorService executor = new ThreadPoolExecutor(
10,
20,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("order-worker-" + thread.getId());
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
每个参数的含义:
| 参数 | 含义 | 新手理解 |
|---|---|---|
corePoolSize |
核心线程数 | 日常保留多少个工人 |
maximumPoolSize |
最大线程数 | 高峰期最多允许多少个工人 |
keepAliveTime |
非核心线程空闲存活时间 | 临时工空闲多久后离开 |
TimeUnit |
时间单位 | 秒、毫秒、分钟等 |
workQueue |
任务队列 | 工人忙不过来时,任务先排队 |
ThreadFactory |
线程工厂 | 给线程命名、设置是否守护线程等 |
RejectedExecutionHandler |
拒绝策略 | 队列满且线程也满时怎么处理 |
其中最容易配错的是前 5 个:核心线程、最大线程、队列、存活时间、拒绝策略。
四、线程池到底怎么执行任务?
这是线程池最关键的逻辑。根据 JDK 官方文档和 Spring 文档,任务提交后的流程可以这样理解:
- 如果当前运行线程数小于
corePoolSize,直接创建新线程执行任务。 - 如果核心线程已满,任务进入队列
workQueue。 - 如果队列也满了,并且当前线程数小于
maximumPoolSize,创建非核心线程执行任务。 - 如果线程数已经达到
maximumPoolSize,队列也满了,触发拒绝策略。
用更生活化的话说:
text
先找核心工人干活
核心工人都忙了,就让任务排队
队列也排满了,就临时加工人
工人也加到上限了,就按拒绝策略处理
这里有一个非常重要的点:不是一上来就把线程扩到 maximumPoolSize。
很多新手以为配置了:
java
corePoolSize = 10
maximumPoolSize = 100
queueCapacity = 10000
线程池就会很快扩到 100 个线程。实际上不会。因为核心线程满了以后,任务会优先进入队列。只有队列满了,才会继续创建核心线程之外的线程。
所以队列容量越大,线程池越不容易扩容到最大线程数。
五、为什么不建议直接用 Executors?
JDK 提供了 Executors.newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 等工厂方法。它们适合教学和简单场景,但在生产服务里要谨慎使用。
Alibaba Java Coding Guidelines 明确建议:线程池应通过 ThreadPoolExecutor 创建,而不是直接使用 Executors,这样开发者能明确线程池运行规则,规避资源耗尽风险。
常见风险如下:
| 方法 | 隐藏风险 |
|---|---|
Executors.newFixedThreadPool(n) |
使用无界队列,任务堆积过多可能 OOM |
Executors.newSingleThreadExecutor() |
同样可能使用很大的队列,单线程处理不过来时任务持续堆积 |
Executors.newCachedThreadPool() |
最大线程数近似不受控,高峰期可能创建大量线程 |
Executors.newScheduledThreadPool(n) |
延迟/周期任务如果执行过慢,也可能堆积 |
生产环境更推荐显式配置:
java
ExecutorService executor = new ThreadPoolExecutor(
8,
16,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("biz-worker-" + thread.getId());
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
显式配置的好处是:线程数、队列长度、拒绝策略都摆在明面上,线上容量风险更可控。
六、拒绝策略怎么选?
当线程池已经达到最大线程数,并且队列也满了,新任务就会被拒绝。JDK 默认提供 4 种策略:
| 策略 | 行为 | 适合场景 |
|---|---|---|
AbortPolicy |
直接抛 RejectedExecutionException |
任务不能丢,提交方必须感知失败 |
CallerRunsPolicy |
由提交任务的线程自己执行 | 希望反压上游,降低提交速度 |
DiscardPolicy |
静默丢弃新任务 | 极少使用,除非任务确实可丢 |
DiscardOldestPolicy |
丢弃队列里最老的任务,再尝试提交 | 实时性强、旧任务价值低的场景 |
例如,一个通知分发线程池可以使用 AbortPolicy,让调用方明确感知"任务提交失败"。
java
@Bean(name = "notificationDispatchExecutor")
public ThreadPoolTaskExecutor notificationDispatchExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("notification-dispatch-");
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
这类任务如果发送失败,需要让调用方知道失败并做重试或释放状态,因此直接抛异常比静默丢任务更安全。
七、Spring Boot 项目中推荐怎么用?
在 Spring Boot 项目中,最常见做法是:
- 在配置类中开启
@EnableAsync。 - 定义多个
ThreadPoolTaskExecutorBean。 - 业务方法通过
@Async("线程池Bean名")指定线程池。 - 需要手动编排异步任务时,把线程池传给
CompletableFuture.runAsync或supplyAsync。
1. 定义线程池 Bean
下面是一个适用于 Spring Boot 3.x 的典型配置:
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "statusRefreshExecutor")
public ThreadPoolTaskExecutor statusRefreshExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(3000);
executor.setThreadNamePrefix("status-refresh-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
这段配置表达了几个信息:
- 这是状态刷新任务专用线程池。
- 常态并发 20,峰值最高 40。
- 最多允许 3000 个任务排队。
- 线程名前缀清晰,日志和线程 dump 里容易定位。
- 服务关闭时最多等待 30 秒,让已经提交的任务尽量完成。
如果你使用的是 Spring Boot 3.2 或更高版本,也可以通过 ThreadPoolTaskExecutorBuilder 创建线程池;但新手先掌握上面这种 Bean 风格配置更直观,也更容易理解每个参数对应的底层含义。
2. 用 @Async 执行异步方法
业务代码中可以这样使用:
java
@Async("notificationDispatchExecutor")
public void sendNotification(...) {
// 异步发送通知逻辑
}
Spring 官方文档说明,@Async 方法被调用后,调用方会立即返回,真实执行会被提交给 Spring TaskExecutor。如果使用 @Async("notificationDispatchExecutor"),Spring 会按这个名字找到对应的 Executor Bean。
但是 @Async 有几个新手很容易踩的坑:
- 必须先启用
@EnableAsync。 - 被
@Async标注的方法必须通过 Spring 代理调用,同一个类内部this.xxx()调用不会生效。 void返回值的方法内部异常不会直接抛给调用方,建议记录日志或配置AsyncUncaughtExceptionHandler。- 如果需要拿返回值,返回类型可以用
Future或CompletableFuture。 - 不建议把事务边界和异步边界混在一起,需要明确异步线程里是否还能拿到上下文。
3. 用 CompletableFuture 指定线程池
需要手动编排异步任务时,可以把指定线程池传给 CompletableFuture:
java
private final ThreadPoolTaskExecutor statusRefreshExecutor;
public StatusRefreshService(
@Qualifier("statusRefreshExecutor") ThreadPoolTaskExecutor statusRefreshExecutor) {
this.statusRefreshExecutor = statusRefreshExecutor;
}
private void submitRefreshTask(List<Long> recordIds, String refreshType) {
try {
CompletableFuture.runAsync(
() -> refreshStatus(recordIds, refreshType),
statusRefreshExecutor
).exceptionally(ex -> {
log.error("异步刷新状态失败, refreshType={}, size={}",
refreshType, recordIds.size(), ex);
return null;
});
} catch (RejectedExecutionException ex) {
log.error("线程池已满,异步刷新状态任务提交失败, refreshType={}, size={}",
refreshType, recordIds.size(), ex);
}
}
这个例子有三个优点:
- 没有使用默认线程池,而是明确指定
statusRefreshExecutor。 - 使用
exceptionally记录异步执行过程中的异常。 - 捕获了任务提交阶段可能出现的
RejectedExecutionException。
这是比较适合生产环境的写法。
八、CompletableFuture.runAsync 不传线程池有什么问题?
很多代码里会出现类似写法:
java
CompletableFuture.runAsync(() -> reportService.generateReport(reportId));
这段代码能运行,但从工程治理角度看,不如显式传入业务线程池。
原因是:CompletableFuture 的异步方法如果不指定 Executor,通常会使用默认异步执行设施,也就是公共 ForkJoinPool.commonPool()。这会带来几个问题:
- 多个业务共用公共线程池,互相影响。
- 线程名不带业务语义,排查问题困难。
- 队列、拒绝策略、监控都不容易按业务隔离。
- 如果任务里有阻塞 IO,可能影响公共池中其他计算型任务。
更推荐的写法是:
java
private final ThreadPoolTaskExecutor reportGenerateExecutor;
public ReportTaskService(
@Qualifier("reportGenerateExecutor") ThreadPoolTaskExecutor reportGenerateExecutor) {
this.reportGenerateExecutor = reportGenerateExecutor;
}
public boolean submitReportTask(Long reportId) {
// 前置校验、状态更新等同步逻辑
CompletableFuture.runAsync(
() -> reportService.generateReport(reportId),
reportGenerateExecutor
).exceptionally(ex -> {
log.error("异步生成报表失败, reportId={}", reportId, ex);
return null;
});
return true;
}
这样线程池资源属于"报表生成"业务,不会和其他异步任务混在一起。
九、线程池大小应该怎么估算?
线程池没有万能参数。需要先判断任务类型。
1. CPU 密集型任务
特点:主要消耗 CPU,比如加密、压缩、复杂计算、图片处理。
建议:
text
线程数 ≈ CPU 核数 或 CPU 核数 + 1
原因:线程太多只会增加上下文切换,不会让 CPU 变多。
2. IO 密集型任务
特点:大量等待数据库、Redis、HTTP、文件、第三方接口。
建议:
text
线程数可以大于 CPU 核数
但不能只看 CPU,还要看:
- 数据库连接池大小。
- 下游接口限流。
- 单任务平均耗时。
- 峰值任务量。
- 是否允许任务排队。
一个常见估算公式是:
text
线程数 ≈ CPU 核数 × (1 + 等待时间 / 计算时间)
例如,一个任务 90ms 在等数据库或 HTTP,10ms 在做本地计算,那么:
text
线程数 ≈ CPU 核数 × (1 + 90 / 10) = CPU 核数 × 10
这个公式只适合估算初始值,最终还是要压测和线上监控来调整。
3. 不要让线程池超过下游承载能力
假设数据库连接池最大只有 30 个连接,但你把某个数据库写入线程池配置成 200 个线程,结果很可能不是更快,而是更多线程阻塞在拿连接上,甚至拖垮数据库。
线程池大小要和下游资源一起看:
text
线程池并发数 <= 下游可承受并发数
这是后端性能优化里很朴素但很重要的一条。
十、任务队列怎么选?
常见队列:
| 队列 | 特点 | 适合场景 |
|---|---|---|
ArrayBlockingQueue |
有界数组队列 | 固定容量、内存可控 |
LinkedBlockingQueue |
链表队列,可有界也可无界 | 常用,但一定要指定容量 |
SynchronousQueue |
不存储任务,直接移交线程 | 类似 newCachedThreadPool,需要谨慎控制最大线程数 |
PriorityBlockingQueue |
优先级队列 | 需要按优先级执行任务 |
DelayQueue |
延迟队列 | 延迟任务、超时任务 |
生产项目里最常见的是:
java
new LinkedBlockingQueue<>(1000)
关键是括号里的容量。不要轻易使用无界队列。
十一、线程池一定要关闭
JDK 官方文档指出,ExecutorService 不再使用时应该关闭,以便释放资源。
普通 Java 代码里建议这样写:
java
public void executeBatch(List<Runnable> tasks) {
ExecutorService executor = new ThreadPoolExecutor(
4,
8,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("batch-worker-" + thread.getId());
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
try {
for (Runnable task : tasks) {
executor.execute(task);
}
} finally {
shutdownAndAwaitTermination(executor);
}
}
private void shutdownAndAwaitTermination(ExecutorService executor) {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
log.error("线程池未能正常关闭");
}
}
} catch (InterruptedException ex) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Spring 管理的 ThreadPoolTaskExecutor 通常由 Spring 容器负责生命周期,但如果你自己 new ThreadPoolExecutor(),就要自己负责关闭。
如果某个组件自己创建了线程池,可以实现 Spring 的 DisposableBean,并在 destroy() 中关闭线程池:
java
@Override
public void destroy() {
eventDispatchExecutor.shutdown();
notificationExecutor.shutdown();
reportGenerateExecutor.shutdown();
}
如果要进一步增强,可以加上 awaitTermination 和中断处理,确保关闭过程更可控。
十二、线程池异常怎么处理?
线程池里的异常分为两类:
1. 任务提交失败
线程池满了,触发拒绝策略,可能抛出 RejectedExecutionException。
建议:
java
try {
executor.execute(task);
} catch (RejectedExecutionException ex) {
log.error("任务提交失败,线程池已满", ex);
// 根据业务决定:返回失败、稍后重试、写入补偿表、降级处理
}
2. 任务执行失败
任务在线程池里执行时抛异常。
如果用 CompletableFuture:
java
CompletableFuture
.runAsync(() -> doSomething(), executor)
.exceptionally(ex -> {
log.error("异步任务执行失败", ex);
return null;
});
如果用 submit:
java
Future<Result> future = executor.submit(() -> queryRemoteData());
try {
Result result = future.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("任务被中断", ex);
} catch (ExecutionException ex) {
log.error("异步任务执行失败", ex.getCause());
throw new IllegalStateException("异步任务执行失败", ex.getCause());
}
注意:execute 和 submit 的异常表现不同。execute 中未捕获异常通常会交给线程的异常处理机制;submit 会把异常包装到 Future,需要调用 get() 时才能感知。
十三、ThreadLocal 和上下文传递要小心
很多后端项目会把用户信息、请求 ID、traceId、登录态放在 ThreadLocal 或 MDC 里。异步线程不是原调用线程,默认拿不到这些上下文。
常见问题:
- 日志 traceId 丢失。
- 用户信息丢失。
- 数据权限判断异常。
如果项目使用阿里 TransmittableThreadLocal,可以通过 TtlExecutors.getTtlExecutorService(...) 包装线程池,在异步线程中传递必要上下文。
示例:
java
ExecutorService executor = TtlExecutors.getTtlExecutorService(
new ThreadPoolExecutor(
8,
16,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
runnable -> {
Thread thread = new Thread(runnable);
thread.setName("event-worker-" + thread.getId());
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
)
);
但要注意:上下文传递不是越多越好。敏感信息、过期上下文、事务对象等不应该随便跨线程传播。
十四、Spring Boot 3.x + JDK 17 的完整案例:异步导出报表
假设有一个报表导出接口。用户点击"导出"后,接口不要一直阻塞到文件生成完成,而是快速返回一个 taskId;后台线程池负责生成文件,并把任务状态更新为成功或失败。
这个案例能体现线程池在真实系统里的价值:
- 慢任务不阻塞 HTTP 请求线程。
- 报表导出有独立线程池,不影响通知发送、状态刷新等其他异步任务。
- 线程池满了时能让调用方感知系统繁忙。
- 后台任务失败时有日志和任务状态兜底。
1. 请求和响应对象
JDK 17 可以使用 record 定义简单 DTO。Spring Boot 3.x 使用 Jakarta EE 规范,参数校验包名是 jakarta.validation。
java
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public record ReportExportRequest(
@NotBlank String reportType,
@NotNull LocalDate startDate,
@NotNull LocalDate endDate
) {
}
public record ReportExportResponse(Long taskId) {
}
2. 配置报表导出线程池
java
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 AsyncConfig {
@Bean(name = "reportExportExecutor")
public ThreadPoolTaskExecutor reportExportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(500);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("report-export-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
这里选择 AbortPolicy,是因为报表导出任务通常不能静默丢弃。如果线程池已经满了,接口层应该明确返回"系统繁忙,请稍后再试",而不是让用户以为任务已经提交成功。
3. Controller 快速返回 taskId
java
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/reports")
public class ReportExportController {
private final ReportExportService reportExportService;
public ReportExportController(ReportExportService reportExportService) {
this.reportExportService = reportExportService;
}
@PostMapping("/exports")
public ReportExportResponse export(@Valid @RequestBody ReportExportRequest request) {
Long taskId = reportExportService.createExportTask(request);
return new ReportExportResponse(taskId);
}
}
Controller 只负责接收请求和返回响应,不直接处理耗时的报表生成逻辑。
4. Service 创建任务并提交异步执行
java
import org.springframework.core.task.TaskRejectedException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
@Service
public class ReportExportService {
private final ReportTaskRepository reportTaskRepository;
private final ReportExportAsyncService reportExportAsyncService;
public ReportExportService(ReportTaskRepository reportTaskRepository,
ReportExportAsyncService reportExportAsyncService) {
this.reportTaskRepository = reportTaskRepository;
this.reportExportAsyncService = reportExportAsyncService;
}
@Transactional(rollbackFor = Exception.class)
public Long createExportTask(ReportExportRequest request) {
validateDateRange(request);
Long taskId = reportTaskRepository.createPendingTask(request);
try {
reportExportAsyncService.generateReport(taskId, request);
} catch (TaskRejectedException ex) {
reportTaskRepository.markFailed(taskId, "系统繁忙,任务提交失败");
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "导出任务繁忙,请稍后再试", ex);
}
return taskId;
}
private void validateDateRange(ReportExportRequest request) {
if (request.endDate().isBefore(request.startDate())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "结束日期不能早于开始日期");
}
}
}
这段代码里,ReportTaskRepository 可以理解为任务表的数据访问对象。真实项目里,你可以用 JPA、MyBatis、JDBC 或任何已有持久化方案实现它。
5. 异步服务执行耗时任务
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
public class ReportExportAsyncService {
private final ReportGenerator reportGenerator;
private final ReportTaskRepository reportTaskRepository;
public ReportExportAsyncService(ReportGenerator reportGenerator,
ReportTaskRepository reportTaskRepository) {
this.reportGenerator = reportGenerator;
this.reportTaskRepository = reportTaskRepository;
}
@Async("reportExportExecutor")
public CompletableFuture<Void> generateReport(Long taskId, ReportExportRequest request) {
try {
reportTaskRepository.markRunning(taskId);
Path filePath = reportGenerator.generate(request);
reportTaskRepository.markSuccess(taskId, filePath.toString());
} catch (Exception ex) {
log.error("报表导出失败, taskId={}, reportType={}", taskId, request.reportType(), ex);
reportTaskRepository.markFailed(taskId, "报表生成失败");
}
return CompletableFuture.completedFuture(null);
}
}
这里把异步方法放到单独的 ReportExportAsyncService,可以避免同类内部调用导致 @Async 不生效。任务执行失败时不要只写日志,还要把任务状态标记为失败,否则用户只能看到"处理中"一直不结束。
十五、业务线程池隔离怎么理解?
真实系统里通常会按业务场景拆分线程池,例如:
java
@Bean(name = "reportGenerateExecutor")
public ThreadPoolTaskExecutor reportGenerateExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("report-generate-");
executor.initialize();
return executor;
}
@Bean(name = "retrySyncExecutor")
public ThreadPoolTaskExecutor retrySyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(2000);
executor.setThreadNamePrefix("retry-sync-");
executor.initialize();
return executor;
}
这体现了一个重要原则:不同业务场景用不同线程池隔离。
为什么要隔离?
- 报表生成任务变慢,不应该拖慢通知发送。
- 文件同步任务堆积,不应该影响状态刷新。
- 批量刷新任务高峰,不应该占满所有异步线程。
retrySyncExecutor 配成单线程也很有业务含义:重试同步任务可能要求顺序执行,或者不希望并发太高影响下游。
十六、常见错误清单
1. 队列无界
错误写法:
java
new LinkedBlockingQueue<>()
建议:
java
new LinkedBlockingQueue<>(1000)
2. 线程池没有业务命名
错误表现:
text
pool-1-thread-1
pool-2-thread-3
建议:
java
executor.setThreadNamePrefix("data-monitor-status-pool-");
线程名应该能看出业务含义。
3. CompletableFuture 不指定线程池
不推荐:
java
CompletableFuture.runAsync(() -> doSomething());
推荐:
java
CompletableFuture.runAsync(() -> doSomething(), bizExecutor);
4. @Async 同类内部调用
不生效:
java
public void methodA() {
this.methodB();
}
@Async("bizExecutor")
public void methodB() {
// 不会异步执行
}
建议把异步方法放到另一个 Spring Bean,通过代理调用。
5. 异步任务异常无人感知
不推荐:
java
CompletableFuture.runAsync(() -> riskyOperation(), executor);
推荐:
java
CompletableFuture
.runAsync(() -> riskyOperation(), executor)
.exceptionally(ex -> {
log.error("异步任务执行失败", ex);
return null;
});
6. 在循环里无节制提交任务
不推荐:
java
for (Item item : items) {
executor.execute(() -> process(item));
}
如果 items 很大,瞬间提交大量任务会冲击线程池和队列。
建议:
- 分批提交。
- 控制分页大小。
- 使用有界队列和拒绝策略。
- 必要时加限流或信号量。
7. 线程池嵌套调用导致死等
比如线程池只有 2 个线程,任务 A 和 B 都在等同一个线程池里的子任务完成,但子任务没有线程可用,就可能卡住。
建议:
- 避免在同一个小线程池内同步等待子任务。
- 计算型任务和 IO 型任务隔离。
- 不要在异步任务里随意
join()大量同池任务。
8. 忽略中断
不推荐:
java
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
log.warn("被中断", ex);
}
推荐:
java
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
log.warn("任务被中断", ex);
}
捕获 InterruptedException 后要恢复中断标记,方便上层感知。
十七、线程池上线前检查表
上线前可以按下面清单检查:
- 是否明确了任务类型:CPU 密集型还是 IO 密集型?
- 是否配置了有界队列?
- 是否设置了业务化线程名前缀?
- 是否设置了合适的拒绝策略?
- 是否捕获并记录异步任务异常?
- 是否处理了
RejectedExecutionException? - 是否考虑了下游数据库、Redis、HTTP 接口承载能力?
- 是否避免
CompletableFuture默认公共线程池? - 是否避免
@Async同类内部调用? - 是否需要传递 traceId、tenantId、userId 等上下文?
- 非 Spring 管理的线程池是否会关闭?
- 是否有监控指标:活跃线程数、队列长度、任务完成数、拒绝次数?
十八、怎么监控线程池?
生产上至少应该关注:
| 指标 | 意义 |
|---|---|
poolSize |
当前线程数 |
activeCount |
正在执行任务的线程数 |
queueSize |
当前排队任务数 |
completedTaskCount |
已完成任务数 |
taskCount |
总任务数 |
| reject count | 拒绝次数,需要自定义统计 |
ThreadPoolTaskExecutor 提供了 getPoolSize()、getActiveCount()、getQueueSize() 等方法,可以暴露给监控系统。
简单示例:
java
public void logExecutorMetrics(ThreadPoolTaskExecutor executor, String executorName) {
log.info("executorName={}, poolSize={}, activeCount={}, queueSize={}",
executorName,
executor.getPoolSize(),
executor.getActiveCount(),
executor.getQueueSize());
}
如果发现 queueSize 长期接近上限,说明任务生产速度大于消费速度,需要扩容、限流、拆分线程池、优化任务耗时,或者检查下游资源。