#线程池故障实战
2020年双十一当天,某电商平台的订单服务出现了严重的请求堆积问题。
监控告警显示:线程池队列堆积超过10万个任务,响应时间从50ms飙升到30s,大量用户反馈下单超时。
技术团队排查后发现:开发同学配置了Executors.newFixedThreadPool(100),队列是LinkedBlockingQueue,默认容量是Integer.MAX_VALUE,看起来不会拒绝任务。
但问题是:100个线程处理10万QPS,每个任务耗时100ms,线程池吞吐量只有1000QPS,根本处理不过来。
这次故障导致订单丢失约5000笔,直接损失约100万元。
【面试官手记】
线程池是Java开发中最容易被错误配置的组件。我面试过的候选人里,能说清楚"线程池参数"的有60%,能说清楚"队列选择"的有30%,能说清楚"拒绝策略"的有20%。线程池配置的关键词是合理容量 + 队列有界。
#一、线程池的七大参数 🔴
#1.1 七大参数详解
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)七大参数详解:
1. corePoolSize:核心线程数
- 常驻线程数,即使空闲也不销毁
- 建议:CPU密集型 = CPU核数 + 1,IO密集型 = CPU核数 × 2
2. maximumPoolSize:最大线程数
- 线程池最多持有的线程数
- 包含核心线程和非核心线程
- 建议:CPU密集型 = CPU核数 + 1,IO密集型 = CPU核数 × 2 + 1
3. keepAliveTime:空闲线程存活时间
- 非核心线程空闲后存活时间
- 可以通过 allowCoreThread TimeOut 设置核心线程超时
4. workQueue:任务队列
- ArrayBlockingQueue:有界队列,需要指定容量
- LinkedBlockingQueue:无界队列,默认Integer.MAX_VALUE
- SynchronousQueue:同步队列,不存储任务
- PriorityBlockingQueue:优先级队列
5. threadFactory:线程工厂
- 用于创建线程
- 可以设置线程名称、优先级、是否为守护线程
6. handler:拒绝策略
- AbortPolicy:抛异常(默认)
- CallerRunsPolicy:由调用方执行
- DiscardPolicy:丢弃任务
- DiscardOldestPolicy:丢弃最老的任务#1.2 常见错误配置
// 错误配置1:无界队列
ExecutorService pool1 = Executors.newFixedThreadPool(100);
// 等价于:
new ThreadPoolExecutor(100, 100, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // Integer.MAX_VALUE!
// 问题:队列无限膨胀,内存溢出
// 错误配置2:单线程池
ExecutorService pool2 = Executors.newSingleThreadExecutor();
// 问题:所有任务串行执行
// 错误配置3:缓存线程池
ExecutorService pool3 = Executors.newCachedThreadPool();
// 问题:线程数无限增长,高并发时创建大量线程
// 错误配置4:队列容量过大
new ThreadPoolExecutor(10, 20, 0L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000000)); // 100万容量!
// 问题:队列太大,任务堆积严重,响应延迟#1.3 正确配置方法
// 正确配置:IO密集型任务
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor orderExecutor() {
// IO密集型:线程数 = CPU核数 × 2
int cpuCores = Runtime.getRuntime().availableProcessors();
int poolSize = cpuCores * 2;
return new ThreadPoolExecutor(
poolSize, // 核心线程数
poolSize * 2, // 最大线程数
60L, // 空闲存活时间
TimeUnit.SECONDS,
// 必须是有限队列,控制任务堆积
new LinkedBlockingQueue<>(1000),
// 自定义线程工厂
new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d")
.setDaemon(false)
.build(),
// 拒绝策略
new ThreadPoolExecutor.AbortPolicy()
);
}
}
// 正确配置:CPU密集型任务
@Bean
public ThreadPoolExecutor computeExecutor() {
int cpuCores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cpuCores + 1,
cpuCores + 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder()
.setNameFormat("compute-pool-%d")
.build(),
new ThreadPoolExecutor.AbortPolicy()
);
}#二、线程池队列选择 🟡
#2.1 队列类型对比
队列类型对比:
1. LinkedBlockingQueue
- 无界队列,容量Integer.MAX_VALUE
- 优点:不拒绝任务
- 缺点:堆积导致内存溢出
- 适用:任务不紧急,允许延迟
2. ArrayBlockingQueue
- 有界队列,需要指定容量
- 优点:可控,不会无限堆积
- 缺点:满时拒绝任务
- 适用:必须控制堆积量
3. SynchronousQueue
- 同步队列,不存储任务
- 优点:不会堆积,必须立即执行
- 缺点:高并发时创建大量线程
- 适用:需要立即执行的任务
4. PriorityBlockingQueue
- 优先级队列
- 优点:按优先级处理任务
- 缺点:无界,需要注意容量
- 适用:优先级任务调度#2.2 队列选择策略
// 队列选择策略
public class QueueSelectionStrategy {
/**
* 根据任务特性选择队列
*/
public BlockingQueue<Runnable> selectQueue(TaskType type, int capacity) {
switch (type) {
case URGENT:
// 紧急任务:SynchronousQueue,必须立即处理
return new SynchronousQueue<>();
case IMPORTANT:
// 重要任务:ArrayBlockingQueue,有界可控
return new ArrayBlockingQueue<>(capacity);
case NORMAL:
// 普通任务:LinkedBlockingQueue,允许一定堆积
return new LinkedBlockingQueue<>(capacity);
case BACKGROUND:
// 后台任务:PriorityBlockingQueue,按优先级
return new PriorityBlockingQueue<>(capacity,
Comparator.comparingInt(Task::getPriority));
default:
return new LinkedBlockingQueue<>(capacity);
}
}
}#三、拒绝策略设计 🟡
#3.1 拒绝策略对比
拒绝策略对比:
1. AbortPolicy(默认)
- 抛RejectedExecutionException
- 优点:调用方能感知任务被拒绝
- 缺点:需要上层处理异常
- 适用:需要严格保证任务执行的场景
2. CallerRunsPolicy
- 由调用方线程执行任务
- 优点:不会丢失任务
- 缺点:调用方线程被阻塞,可能影响主流程
- 适用:不允许任务丢失,但可以接受延迟
3. DiscardPolicy
- 直接丢弃任务
- 优点:不会阻塞
- 缺点:任务丢失
- 适用:不重要的任务
4. DiscardOldestPolicy
- 丢弃队列中最老的任务
- 优点:腾出空间给新任务
- 缺点:老任务丢失
- 适用:优先处理新任务的场景#3.2 自定义拒绝策略
// 自定义拒绝策略:记录+告警+降级
public class LoggingRejectHandler implements RejectedExecutionHandler {
@Autowired
private AlertService alertService;
@Autowired
private MetricsService metricsService;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志
log.error("任务被拒绝,线程池状态:活跃={}, 队列大小={}, 核心={}, 最大={}",
executor.getActiveCount(),
executor.getQueue().size(),
executor.getCorePoolSize(),
executor.getMaximumPoolSize());
// 2. 发送告警
alertService.sendAlert("线程池拒绝任务,当前队列堆积:" +
executor.getQueue().size());
// 3. 记录指标
metricsService.increment("threadpool.rejected");
// 4. 降级处理
if (r instanceof Callable) {
try {
// 降级:同步执行,设置超时
Object result = ((Callable<?>) r).call();
log.warn("降级执行成功");
} catch (Exception e) {
log.error("降级执行失败", e);
}
}
}
}
// 降级策略:写入消息队列
public class MQRejectHandler implements RejectedExecutionHandler {
@Autowired
private MessageProducer producer;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof TaskRunnable) {
TaskRunnable task = (TaskRunnable) r;
// 写入消息队列,稍后处理
producer.send("task.retry", task.getTaskData());
log.warn("任务写入MQ重试队列");
}
}
}#四、线程池监控 🟡
#4.1 线程池指标采集
// 线程池监控
@Component
@Slf4j
public class ThreadPoolMonitor {
@Autowired
private Map<String, ThreadPoolExecutor> threadPools;
@Scheduled(fixedRate = 10000)
public void monitor() {
for (Map.Entry<String, ThreadPoolExecutor> entry : threadPools.entrySet()) {
String name = entry.getKey();
ThreadPoolExecutor pool = entry.getValue();
int activeCount = pool.getActiveCount();
int poolSize = pool.getPoolSize();
int queueSize = pool.getQueue().size();
long completedTaskCount = pool.getCompletedTaskCount();
long totalTaskCount = pool.getTaskCount();
// 计算使用率
double activeRate = (double) activeCount / poolSize;
double queueUsage = (double) queueSize / 1000; // 假设队列容量1000
log.info("线程池[{}]:活跃={}/{}, 队列={}, 完成={}/{}",
name, activeCount, poolSize, queueSize,
completedTaskCount, totalTaskCount);
// 告警条件
if (activeRate > 0.8) {
alert("线程池[{}]活跃率超过80%", name);
}
if (queueSize > 800) {
alert("线程池[{}]队列堆积超过80%", name);
}
}
}
}#4.2 动态调整线程池
// 动态调整线程池
@Service
public class DynamicThreadPool {
@Autowired
private ConfigService configService;
public void adjustThreadPool(String poolName, ThreadPoolExecutor pool) {
// 从配置中心获取新的配置
int coreSize = configService.getInt(poolName + ".coreSize", 10);
int maxSize = configService.getInt(poolName + ".maxSize", 20);
// 调整核心线程数
pool.setCorePoolSize(coreSize);
// 调整最大线程数
pool.setMaximumPoolSize(maxSize);
// 设置核心线程超时
pool.allowCoreThreadTimeOut(
configService.getBoolean(poolName + ".allowTimeout", true));
log.info("线程池[{}]已调整:核心={}, 最大={}",
poolName, coreSize, maxSize);
}
}#五、生产避坑 🟡
#5.1 线程池的五大坑
坑1:使用Executors创建线程池
问题:Executors创建的是无界队列或无限线程
场景:newFixedThreadPool队列容量Integer.MAX_VALUE
解决方案:
- 使用ThreadPoolExecutor自定义配置
- 指定队列容量
- 配置拒绝策略坑2:队列容量设置过大
问题:队列太大,任务堆积严重
场景:队列容量100万
解决方案:
- 根据业务设置合理容量
- 监控队列堆积量
- 超过阈值告警坑3:线程数设置不合理
问题:CPU密集型配置了大量线程
场景:8核CPU配置50线程
解决方案:
- CPU密集型:线程数 = CPU核数 + 1
- IO密集型:线程数 = CPU核数 × 2坑4:没有拒绝策略
问题:任务被拒绝但没有处理
场景:AbortPolicy默认抛异常
解决方案:
- 自定义拒绝策略
- 记录日志+发送告警
- 降级处理坑5:没有监控
问题:线程池状态不透明,出问题才发现
场景:队列堆积到10万才发现
解决方案:
- 监控线程池指标
- 监控队列堆积量
- 设置告警阈值#5.2 线程池检查清单
配置规范:
- [ ] 不使用Executors创建线程池
- [ ] 队列容量必须设置
- [ ] 拒绝策略必须配置
- [ ] 线程数根据任务类型配置
监控规范:
- [ ] 监控活跃线程数
- [ ] 监控队列堆积量
- [ ] 监控任务执行时间
- [ ] 告警阈值设置合理
代码规范:
- [ ] 任务必须实现接口
- [ ] 任务必须可序列化
- [ ] 异常必须捕获处理#六、真实面试回放 🟡
面试官:线程池参数有哪些?怎么配置?
候选人(小张):七大参数:
corePoolSize核心线程数、maximumPoolSize最大线程数、keepAliveTime空闲存活时间、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。
配置的话,CPU密集型配置核心线程数为CPU核数加1,IO密集型配置为CPU核数乘以2。
面试官:LinkedBlockingQueue和ArrayBlockingQueue区别?
小张:LinkedBlockingQueue是无界的,默认容量是Integer.MAX_VALUE,容易堆积。ArrayBlockingQueue是有界的,必须指定容量,不会无限堆积。
面试官:拒绝策略有哪些?
小张:四种:
AbortPolicy抛异常,CallerRunsPolicy由调用方执行,DiscardPolicy丢弃任务,DiscardOldestPolicy丢弃最老的任务。
我一般用AbortPolicy,配合告警。
【面试官手记】
小张这场面试的亮点:
知道七大参数
知道队列类型区别
知道拒绝策略种类
线程池是P6工程师必备知识点,能完整回答的候选人,说明有生产调优经验。
线程池配置的核心是合理容量 + 队列有界。记住三个要点:
- 队列必须有界:防止内存溢出
- 线程数合理:CPU密集型=核数+1,IO密集型=核数×2
- 拒绝策略必须配置:记录日志+发送告警+降级处理
线程池是生产环境的性能关键,配错了轻则响应慢,重则OOM。