1. 为什么@Async可能成为服务器资源杀手
在Spring Boot项目中,@Async注解确实是个好东西——它能让方法异步执行,提升接口响应速度。但去年我们生产环境的一次事故让我深刻认识到:用不好的@Async就像在服务器里埋了颗定时炸弹。当时一个核心服务突然CPU飙到100%,排查后发现正是某个@Async方法疯狂创建线程导致的。
1.1 默认线程池的陷阱
Spring的@Async默认使用SimpleAsyncTaskExecutor,这个执行器有个致命特点:来一个任务就新建一个线程。我们来看段典型问题代码:
java复制@Async
public void processData(List<Data> dataList) {
dataList.forEach(data -> {
// 复杂数据处理逻辑
});
}
当并发请求量达到1000时,JVM会瞬间创建1000个线程。我见过最夸张的案例是线程数突破2万,直接导致OOM。这就像在餐厅里每来一个顾客就新开一家分店,迟早把整条街吃垮。
1.2 线程池参数配置的学问
正确的做法是自定义线程池。关键参数有四个:
- corePoolSize:核心线程数(常驻"员工"数量)
- maxPoolSize:最大线程数(临时工上限)
- queueCapacity:队列容量(等候区大小)
- keepAliveSeconds:空闲线程存活时间(临时工下班时间)
java复制@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("Async-Executor-");
executor.initialize();
return executor;
}
}
重要提示:队列类型建议使用LinkedBlockingQueue而不是SynchronousQueue,后者会导致超出核心线程数就立即创建新线程,失去缓冲作用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程池使用中的隐蔽坑点
2.1 上下文丢失问题
使用@Async时,很多开发者会遇到RequestContextHolder.getRequestAttributes()返回null的情况。这是因为异步线程默认不会继承父线程的上下文。解决方法有两种:
- 使用DelegatingSecurityContextAsyncTaskExecutor(Spring Security环境)
- 手动传递上下文:
java复制@Async
public void asyncMethod(RequestAttributes attributes) {
RequestContextHolder.setRequestAttributes(attributes);
// 业务逻辑
}
// 调用处
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
asyncService.asyncMethod(attributes);
2.2 线程复用导致的数据污染
使用ThreadLocal时更要小心。我曾遇到过一个诡异bug:A用户看到了B用户的数据。原因就是线程池复用线程时,没有清理ThreadLocal数据。解决方案:
- 每次使用完手动remove()
- 使用阿里开源的TransmittableThreadLocal
- 在afterExecute中清理:
java复制executor.setTaskDecorator(runnable -> {
// 执行前备份
Map<String, Object> context = backupContext();
return () -> {
try {
// 执行前恢复
restoreContext(context);
runnable.run();
} finally {
// 执行后清理
cleanContext();
}
};
});
3. 生产环境监控与调优
3.1 线程池监控指标
光配置好线程池还不够,必须建立监控体系。关键指标包括:
- 活跃线程数(activeCount)
- 队列剩余容量(remainingCapacity)
- 已完成任务数(completedTaskCount)
- 拒绝任务数(rejectedExecutionCount)
推荐使用Micrometer暴露这些指标:
java复制@Bean
public MeterBinder threadPoolMetrics(ThreadPoolTaskExecutor executor) {
return registry -> {
Gauge.builder("thread.pool.active", executor::getActiveCount)
.register(registry);
Gauge.builder("thread.pool.queue.remaining", executor::getQueueSize)
.register(registry);
};
}
3.2 动态调参技巧
线上环境流量波动大,固定参数可能不够灵活。我们可以实现动态调整:
java复制@RestController
@RequestMapping("/thread-pool")
public class ThreadPoolController {
@Autowired
private ThreadPoolTaskExecutor executor;
@PostMapping("/adjust")
public String adjustPool(
@RequestParam(required = false) Integer coreSize,
@RequestParam(required = false) Integer maxSize) {
if (coreSize != null) {
executor.setCorePoolSize(coreSize);
}
if (maxSize != null) {
executor.setMaxPoolSize(maxSize);
}
return "当前配置:core=" + executor.getCorePoolSize()
+ ", max=" + executor.getMaxPoolSize();
}
}
实际调整时要注意:调大核心线程数会立即生效,调小要等空闲线程超时;最大线程数调整对已创建的线程无效。
4. 最佳实践与避坑指南
4.1 业务隔离原则
不要所有@Async方法共用同一个线程池!根据业务特性划分:
- CPU密集型:线程数≈CPU核数
- IO密集型:线程数可适当放大
- 高优先级业务:独立线程池避免被拖累
java复制@Bean(name = "orderAsyncExecutor")
public Executor orderAsyncExecutor() {
// 订单相关异步任务
}
@Bean(name = "reportAsyncExecutor")
public Executor reportAsyncExecutor() {
// 报表生成异步任务
}
// 使用指定执行器
@Async("orderAsyncExecutor")
public void processOrder() {...}
4.2 异常处理机制
@Async方法抛异常时,默认只会打印日志。建议全局处理:
java复制@Configuration
public class AsyncExceptionConfig implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// 发送告警邮件/短信
AlertManager.notify(ex, method.getName());
// 记录详细错误日志
log.error("Async方法执行失败: {} - {}", method.getName(), Arrays.toString(params), ex);
}
}
4.3 优雅关闭策略
应用关闭时,正在执行的异步任务可能被强行终止。解决方法:
- 实现SmartLifecycle控制关闭顺序
- 设置等待时间:
java复制@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30); // 等待30秒
return executor;
}
5. 真实案例复盘
去年双十一大促期间,我们的优惠券系统出现过这样的事故:
- 现象:接口超时率突然飙升到50%
- 排查:发现有个@Async方法在处理10万条数据时,创建了500个线程
- 根因:开发没有设置队列容量,导致超出核心线程数就不断新建线程
- 解决:改用固定大小线程池+批量处理:
java复制@Async("batchExecutor")
public CompletableFuture<Void> batchProcess(List<Data> dataList) {
// 每100条一批处理
Lists.partition(dataList, 100).forEach(batch -> {
// 处理逻辑
});
return CompletableFuture.completedFuture(null);
}
这个案例给我们的教训是:异步不等于无限制并发。对于批量操作,应该控制并发粒度。
6. 高级优化技巧
6.1 线程池预热
系统刚启动时,线程池是空的,突发流量可能导致大量任务排队。可以预先启动核心线程:
java复制@Bean
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ...其他配置
executor.setAllowCoreThreadTimeOut(false); // 核心线程不允许超时
executor.initialize();
executor.prestartAllCoreThreads(); // 启动所有核心线程
return executor;
}
6.2 动态队列切换
根据监控指标自动切换队列策略:
- 低负载时:用无界队列提高吞吐
- 高负载时:换有界队列保护系统
java复制public class DynamicBlockingQueue<E> extends LinkedBlockingQueue<E> {
private volatile boolean bounded = false;
@Override
public boolean offer(E e) {
if (bounded) {
return super.offer(e);
} else {
// 无界队列直接插入
super.put(e);
return true;
}
}
public void setBounded(boolean bounded) {
this.bounded = bounded;
}
}
7. 其他注意事项
- 避免异步嵌套:一个@Async方法调用另一个@Async方法会导致线程数指数级增长
- 超时控制:长时间运行的异步任务应该设置超时:
java复制@Async
public CompletableFuture<Void> longRunningTask() {
return CompletableFuture.runAsync(() -> {
try {
// 业务逻辑
} catch (Exception e) {
throw new CompletionException(e);
}
}).orTimeout(30, TimeUnit.SECONDS); // 30秒超时
}
- 资源清理:异步操作中打开的数据库连接、文件流等必须显式关闭,不能依赖线程结束自动释放
通过合理配置和这些优化手段,我们最终将系统的线程数控制在50以内,同时异步任务处理能力提升了3倍。关键是要记住:异步是手段不是目的,真正的目标是在系统稳定的前提下提高吞吐量。
