Java面试要点:线程池打满、任务堆积、拒绝策略与参数调优
前言
线程池打满会导致接口慢、任务不执行、队列堆积甚至内存溢出。面试中常问:线程池参数怎么配?队列满了怎么办?为什么不能所有业务共用一个线程池?
典型现象
- 异步任务延迟执行。
- 接口卡在 Future.get。
- 日志出现 RejectedExecutionException。
- 队列长度持续增长。
- CPU 不高但请求很慢。
- 某个慢任务拖垮所有异步任务。
线程池执行模型
flowchart LR
A[提交任务] --> B{核心线程是否满}
B -->|否| C[创建核心线程]
B -->|是| D{队列是否满}
D -->|否| E[进入队列]
D -->|是| F{最大线程是否满}
F -->|否| G[创建非核心线程]
F -->|是| H[执行拒绝策略]
排查指标
线程池必须监控:
- corePoolSize。
- maximumPoolSize。
- activeCount。
- poolSize。
- queueSize。
- completedTaskCount。
- rejectCount。
- taskCost。
没有这些指标,线上只能靠日志猜。
常见根因
队列过大
很多系统把队列设置成几万甚至无界队列。问题是任务堆积时不会立即失败,而是延迟越来越大,最终可能 OOM。
建议:
- 使用有界队列。
- 配置拒绝策略。
- 监控队列长度。
- 对延迟敏感任务单独线程池。
慢任务占满线程
例如发送通知线程池里混入了文件上传、第三方调用、报表生成。慢任务占住线程后,普通通知也发不出去。
处理方式:
- 按业务隔离线程池。
- IO 密集和 CPU 密集分开。
- 核心链路和非核心链路分开。
拒绝策略不合理
常见拒绝策略:
- AbortPolicy:直接抛异常。
- CallerRunsPolicy:调用线程执行。
- DiscardPolicy:直接丢弃。
- DiscardOldestPolicy:丢弃最旧任务。
生产中不能随便丢任务。重要任务要落库或发 MQ,拒绝后可补偿。
Future 阻塞
异步任务提交后立刻 get(),本质上还是同步等待。多个任务相互等待时还可能造成死锁。
处理方式:
- 使用 CompletableFuture 编排。
- 设置超时时间。
- 避免线程池内任务等待同一个线程池的其他任务。
参数调优思路
线程池参数要结合任务类型:
| 任务类型 | 特征 | 配置思路 |
|---|---|---|
| CPU 密集 | 计算多 | 线程数接近 CPU 核数 |
| IO 密集 | 等待多 | 线程数可适当增大 |
| 第三方调用 | 延迟不可控 | 独立线程池、短超时 |
| 批处理 | 吞吐优先 | 控制批量和并发 |
参数不是一次配置永久不变,要通过压测和线上指标调整。
生产止血
- 暂停低优先级任务。
- 增加消费者实例或线程池大小。
- 对入口限流,避免继续堆积。
- 临时缩短第三方调用超时。
- 将任务转 MQ 异步削峰。
- 对可丢弃任务降级。
长期治理
- 线程池统一封装和命名。
- 指标上报到监控系统。
- 设置队列告警和拒绝告警。
- 业务线程池隔离。
- 任务超时控制。
- 拒绝后补偿机制。
高频面试题
线程池打满怎么排查?
看 activeCount、queueSize、rejectCount 和任务耗时,再结合线程栈确认线程在执行什么任务,是慢任务、阻塞等待、下游超时还是参数配置不合理。
队列越大越好吗?
不是。队列过大会掩盖问题,导致延迟持续增加,严重时 OOM。生产上更推荐有界队列、监控和明确拒绝策略。
为什么要线程池隔离?
不同业务的耗时和优先级不同。共用线程池会导致低优先级慢任务拖垮核心任务,隔离后可以单独限流、降级和扩容。
总结
线程池排查重点是任务是否堆积、线程在执行什么、拒绝后怎么处理。面试回答要能从参数讲到隔离、监控、止血和补偿。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Junly博客!
评论


