一次 @Async 注解引发的线程池阻塞问题排查与解决

作者 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() 阻塞等待,实际上和同步没有区别,反而额外增加了线程切换开销。

经验教训:

  1. 使用 @Async 时,调用方不能同步等待结果,否则请直接同步调用
  2. 线程池必须设置超时和拒绝策略,不能裸奔
  3. 批量任务要控制节奏,不能短时间大量提交
  4. 异步不等于无感知,异常和超时处理必须完善

⚠️ 声明:本文相关内容仅供参考,实际问题需结合具体业务场景和代码逻辑进行分析。

发表评论

苏ICP备18039580号-2