线程池的拒绝策略有哪些?
2026年01月11日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官询问线程池的拒绝策略,其核心考察点远不止于让你背诵四种策略的名称。他/她更希望了解:
- 对线程池运行原理的掌握:你是否理解任务被拒绝的前置条件(队列满且线程数达到最大值)?
- 对每种策略内涵的理解与应用场景判断:你是否清楚每种策略的具体行为,并能根据业务场景(如重要性、可丢弃性、实时性要求)做出合理选择?
- 实战经验与设计思想:你是否在实际项目中配置或自定义过拒绝策略?这反映了你的问题排查、资源管理和系统保护意识。
核心答案
当线程池的任务队列已满,且工作线程数已达到最大线程数 maximumPoolSize 时,新提交的任务将会触发拒绝策略(RejectedExecutionHandler)。
JDK 的 ThreadPoolExecutor 类内置了四种拒绝策略,均为其内部类:
- AbortPolicy(中止策略,默认策略):直接抛出
RejectedExecutionException异常,由调用者捕获处理。 - CallerRunsPolicy(调用者运行策略):不抛弃任务,也不抛出异常,而是将任务回退给调用者(即提交任务的线程)执行。
- DiscardPolicy(丢弃策略):静默地丢弃新提交的任务,不抛异常,也无任何通知。
- DiscardOldestPolicy(丢弃最老策略):丢弃队列头部的(即下一个将要被执行的最老)任务,然后尝试重新提交当前新任务。
深度解析
原理与机制
拒绝策略是线程池的最后一道安全阀。其设计遵循了 “饱和对策” 这一通用系统设计原则,旨在处理过载情况。ThreadPoolExecutor 的 execute(Runnable command) 方法在无法入队且无法创建新线程时,会调用 rejectedExecution(command, this) 方法,这正是拒绝策略的逻辑入口。
代码示例与使用方式
import java.util.concurrent.*;
public class RejectionPolicyDemo {
public static void main(String[] args) {
// 创建一个核心线程为2,最大线程为4,队列容量为2的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
4, // maximumPoolSize
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2) // 容量为2的有界队列
// 可以通过最后一个参数显式指定拒绝策略,不指定则默认为 AbortPolicy
//, new ThreadPoolExecutor.AbortPolicy()
);
// 模拟提交超过处理能力的任务(共提交7个任务,最多处理6个:4个线程+2个队列)
for (int i = 1; i <= 7; i++) {
final int taskId = i;
try {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskId);
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
});
} catch (RejectedExecutionException e) {
// 使用 AbortPolicy 时,第7个任务会触发此异常
System.err.println("任务 " + taskId + " 被拒绝: " + e.getMessage());
}
}
executor.shutdown();
}
}
对比分析与最佳实践
| 策略 | 行为 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| AbortPolicy | 抛出异常 | 明确失败反馈,便于上层业务进行降级、告警或重试。 | 若调用方未处理异常,可能导致流程中断。 | 通用型、关键业务。这是默认策略,因为它强制开发者关注并处理过载问题。 |
| CallerRunsPolicy | 调用者执行 | 实现一种简单的负反馈。提交任务的线程被占用执行任务,自然会降低新任务的提交速度,给线程池喘息之机。任务不会被丢失。 | 可能阻塞调用者(如Tomcat的HTTP处理线程),影响整体吞吐。 | 不允许任务丢失、且任务的执行耗时可控的场景。如一些计算密集型任务。 |
| DiscardPolicy | 静默丢弃 | 简单,无额外开销。 | 任务数据无声无息丢失,难以追踪和排查问题。 | 不重要的、可容忍丢失的异步任务,如无关紧要的统计日志。需慎用。 |
| DiscardOldestPolicy | 丢弃队头 | 尝试为最新的任务让出空间。 | 会丢弃一个正在等待的、相对较老的任务,可能导致业务逻辑不完整。 | 后提交的任务优先级更高,且可以容忍丢弃老任务的场景。如实时消息流,新消息比旧消息价值更高。 |
最佳实践:
- 理解默认策略:默认使用
AbortPolicy是 JDK 的良苦用心,提醒你线程池不是无底洞,需要合理配置和监控。 - 自定义策略是常见做法:对于复杂业务,自定义拒绝策略往往是最佳选择。例如,将拒绝的任务持久化到数据库或消息队列,待负载降低后重新投递;或记录详细日志并触发告警。
public class CustomRejectionHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 1. 记录日志(任务信息、线程池状态) // 2. 持久化任务到Redis/Kafka // 3. 触发监控告警 System.err.println("任务被拒绝,已记录并告警。"); // 注意:在此 handler 中执行耗时操作需谨慎,因为它运行在提交任务的线程中 } } - 配置与监控:合理设置
corePoolSize、maximumPoolSize和workQueue容量是根本。同时,应通过JMX或监控系统对线程池的活跃线程数、队列大小等指标进行监控。
常见误区
- 误区一:认为只要用了线程池,任务就一定会被执行。事实:错误的配置和不匹配的拒绝策略会导致任务被丢弃或系统崩溃。
- 误区二:盲目使用
DiscardPolicy或DiscardOldestPolicy以求 “系统稳定”。事实:这掩埋了问题,可能导致核心业务数据丢失,使得线上问题更难被发现和追溯。
总结
四种拒绝策略本质上是 “快速失败”、“延迟降级”、“静默丢弃”和“弃老保新” 四种不同过载处理哲学的实现;选择时,务必结合业务容忍度、任务重要性进行权衡,自定义策略通常是满足复杂业务需求的更优解。