深浅主题
线程池耗尽
问题现象
请求排队时间持续上升,异步任务长时间不启动,出现 RejectedExecutionException;CPU 可能并不高。
适用环境
JDK 17+、Spring ThreadPoolTaskExecutor、阻塞式 HTTP 或数据库访问。
本文是可复用的排障示例,不代表某次个人生产事故。命令中的名称、账号和数据均为示例,执行前请替换为已确认的目标。
复现或诊断步骤
- 同时采集活跃线程数、队列长度、完成速率、拒绝次数和任务耗时分位数。
- 在有权限的测试或生产诊断窗口用 jcmd PID Thread.print,间隔数秒采集多份线程栈。
- 判断线程是在计算、等待连接、远程读取还是互相等待;对照下游延迟与连接池指标。
原因分析
线程被慢 I/O 长时间占用,任务到达速度高于完成速度。无界队列会把压力变成内存和等待时间;同一线程池中的任务提交子任务再同步等待,也可能发生线程饥饿。
修复示例
java
@Bean("notifyExecutor")
public ThreadPoolTaskExecutor notifyExecutor() {
var executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notify-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(20);
return executor;
}示例容量只是起点,应结合 CPU、下游配额、任务平均耗时和允许等待时间压测。给远程调用配置连接与读取超时,隔离不同工作负载。提交方必须处理拒绝:返回可识别错误或落入持久化重试,不能静默丢任务。
验证方法
在限定流量下模拟慢下游,确认队列有上限、拒绝可观测、故障解除后能恢复。记录调参前后吞吐、P95 延迟和下游负载;恢复旧配置需要重启时纳入维护窗口。
常见误区
盲目扩大线程数可能先耗尽数据库连接。CallerRunsPolicy 会把任务转移到调用线程,必须评估其阻塞接口或持锁线程的后果。