作者 lucy · 2026-04-11
⚠️ 声明:本文相关内容仅供参考,实际效果因场景不同可能差异很大,请结合自身情况判断,谨慎参考。
在某次项目迭代上线后,用户反馈系统后台管理页面加载缓慢,部分请求超时严重。登录服务器查看日志,发现大量接口响应时间从正常的 200ms 飙升到 30s 以上,线程堆栈显示多个线程处于 WAITING 状态。问题集中在每日定时任务执行期间,系统几乎处于半瘫痪状态。
问题现象
- 后台接口响应时间从 200ms 上升到 30s+
- 日志中出现大量 HystrixRuntimeException 超时异常
- 定时任务执行期间,核心接口集体变慢
- 服务器 CPU 和内存使用率正常,但线程数异常增长
排查过程
第一步:查看线程堆栈
jstack 进程PID > thread_dump.txt
发现大量线程处于 WAITING 状态,堆栈指向 ReportService 的 Future.get() 调用。
第二步:定位到 ReportService
@Service
public class ReportService {
@Async
public void generateAsyncReport(Long userId) {
// 异步生成报表
}
public Map<String, Object> getReport(Long userId) {
Future<String> future = asyncService.submit(() -> {
return buildReportData(userId);
});
String result = future.get(); // 这里阻塞了!
return JSON.parseObject(result);
}
}
发现问题:@Async 注解虽然标注了方法,但 getReport 方法内部通过 Future.get() 同步等待异步任务结果,且没有设置超时时间。定时任务批量提交了数百个异步任务,而默认线程池的核心线程数只有 8。
第三步:检查线程池配置
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100); // 队列容量仅100
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
根因确认:
- 定时任务批量提交了 500+ 个异步任务
- 线程池核心线程数 8,队列容量 100
- 第 101 个任务开始被拒绝,但没做拒绝策略
- 主线程通过 Future.get() 无限等待,导致线程资源耗尽
解决方案
方案一:设置超时 + 合理线程池参数(快速修复)
public Map<String, Object> getReport(Long userId) {
Future<String> future = asyncService.submit(() -> {
return buildReportData(userId);
});
try {
// 设置5秒超时,避免无限等待
String result = future.get(5, TimeUnit.SECONDS);
return JSON.parseObject(result);
} catch (TimeoutException e) {
log.warn("报表生成超时,userId={}", userId);
return getDefaultReport(userId);
}
}
线程池调整:
executor.setCorePoolSize(20);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(500);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
方案二:重构为同步批量处理(彻底优化)
@Service
public class BatchReportService {
public void generateDailyReports(List<Long> userIds) {
List<List<Long>> partitions = Lists.partition(userIds, 50);
for (List<Long> batch : partitions) {
batch.parallelStream().forEach(this::generateSingleReport);
try {
Thread.sleep(2000); // 每批间隔2秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void generateSingleReport(Long userId) {
String data = buildReportData(userId);
reportDao.save(userId, data);
}
}
总结
| 问题点 | 修复方案 |
|---|---|
| Future.get() 无超时 | 添加 get(timeout, unit) + 超时降级逻辑 |
| 线程池容量过小 | 调大核心线程数和队列容量 |
| 无拒绝策略 | 添加 CallerRunsPolicy |
| 批量任务逐个异步提交 | 改为分页批量 + 控制并发节奏 |
这次问题的本质是:误用异步,把同步等待塞进了所谓”异步”流程里,导致线程池被耗尽。@Async 注解虽然写上了,但调用方用 Future.get() 阻塞等待,实际上和同步没有区别,反而额外增加了线程切换开销。
经验教训:
- 使用 @Async 时,调用方不能同步等待结果,否则请直接同步调用
- 线程池必须设置超时和拒绝策略,不能裸奔
- 批量任务要控制节奏,不能短时间大量提交
- 异步不等于无感知,异常和超时处理必须完善
⚠️ 声明:本文相关内容仅供参考,实际问题需结合具体业务场景和代码逻辑进行分析。