线程池的拒绝策略有哪几种?分别适合什么场景?
简化版
当线程池的队列满了且线程数已达 maximumPoolSize,再提交任务就会触发拒绝策略。JDK 内置四种:AbortPolicy(默认,直接抛 RejectedExecutionException)、CallerRunsPolicy(让提交任务的线程自己执行,形成「反压」)、DiscardPolicy(悄悄丢弃新任务,不报错)、DiscardOldestPolicy(丢掉队列里最老的任务,再尝试提交新的)。还能实现 RejectedExecutionHandler 自定义(如落库、写日志、降级)。选哪种取决于业务:任务不能丢用 AbortPolicy 或 CallerRunsPolicy,可容忍丢弃用 Discard 系列。
详细版
触发时机:只有「队列满 且 线程数 = maximumPoolSize」这一步才触发拒绝——它是线程池过载的最后一道关卡。
四种内置策略:
| 策略 | 行为 | 特点 | 适用 |
|---|---|---|---|
AbortPolicy(默认) | 抛 RejectedExecutionException | 调用方能感知失败 | 关键任务,需要明确知道被拒 |
CallerRunsPolicy | 提交任务的线程自己执行该任务 | 减缓提交速度(反压),不丢任务 | 不允许丢任务、能接受变慢 |
DiscardPolicy | 静默丢弃新任务 | 不抛异常、无感知 | 可容忍丢失的任务(如日志上报) |
DiscardOldestPolicy | 丢弃队列最老任务,重试提交新任务 | 保新弃旧 | 新任务比旧任务更有价值(如实时数据) |
// 指定拒绝策略
new ThreadPoolExecutor(4, 8, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()); // 反压策略
// 自定义:拒绝时落库 + 告警,事后补偿
RejectedExecutionHandler custom = (task, executor) -> {
log.warn("任务被拒,落库补偿");
saveToDb(task); // 存起来,稍后重试
alert("线程池过载");
};
⚠️ 默认是
AbortPolicy——它抛的是非受检异常RejectedExecutionException。如果调用处没 try-catch,任务被拒会直接把提交线程的当前流程打断。用execute提交时异常直接抛出,用submit提交时异常被包进 Future,get()时才抛。
完整版教学
一、拒绝策略在哪一步触发:过载的最后一关
要理解拒绝策略,先定位它在线程池处理流程的位置:
提交任务 execute(task):
① 线程数 < core → 建核心线程执行
② core 满 → 进队列排队
③ 队列满 & 线程数 < max → 建非核心线程执行
④ 队列满 & 线程数 = max → 触发拒绝策略 ★ ← 就在这里
拒绝策略是系统扛不住时的兜底动作:核心线程、队列、非核心线程三道缓冲全满了,说明提交速度远超处理能力。这时线程池必须做个决定——是抛异常、还是丢任务、还是拖慢提交方。选哪种,本质是回答「过载时,什么对业务伤害最小」。所以没有「最好的策略」,只有「最匹配业务语义的策略」。
二、AbortPolicy:默认的「快速失败」
默认策略,直接抛 RejectedExecutionException:
队列满+线程满 → 新任务被拒 → throw RejectedExecutionException
调用方:能立刻感知"我这个任务没被接受"
它的价值是不隐藏问题——过载时立刻报错,调用方可以据此重试、降级、返回错误页。适合「任务重要,被拒了必须知道」的场景。风险是:如果调用处没有捕获这个异常,它会中断当前请求流程;而且在高并发下大量抛异常本身有开销。所以用默认策略时,提交处应有 try-catch 和降级逻辑,不能裸提交。
三、CallerRunsPolicy:最巧妙的「反压」
这是设计最精妙的一种。它不抛异常、不丢任务,而是让提交任务的那个线程自己去执行这个任务:
主线程(提交者)不停 execute(task):
线程池满了 → CallerRunsPolicy → 主线程亲自执行这个 task(耗时 T)
→ 主线程被"占用"了 T 时间,这段时间它无法提交新任务
→ 提交速度自然降下来,给线程池喘息时间(这就是"反压 back-pressure")
效果像一个自动的「限速阀」:线程池越忙,提交方越被拖慢,形成负反馈,避免任务无限涌入。而且任务一个都不丢(提交方兜底执行了)。代价是提交线程(可能是 Tomcat 的请求线程)被占用,会拖慢整体响应,甚至如果提交线程是单线程且任务很慢,可能造成阻塞。它特别适合「任务不能丢、能接受偶尔变慢」的批处理、数据导入场景。
四、Discard 与 DiscardOldest:可丢弃场景的取舍
两种「丢任务」策略,区别在丢新的还是丢旧的:
DiscardPolicy: 新任务直接丢,静默无声(不抛异常、无日志)
队列: [老1,老2,老3,老4,老5](满) 新任务 → 丢弃,什么都不发生
DiscardOldestPolicy: 丢掉队列头部最老的,腾位给新任务
队列: [老1,老2,老3,老4,老5](满) 新任务来:
→ 丢掉 老1 → 队列变 [老2,老3,老4,老5,新] → 新任务入队
选择依据是任务的时效性:
- 如果新数据比旧数据有价值(如实时行情、传感器最新读数——旧的过时了),用 DiscardOldest 保新弃旧;
- 如果任务同等价值且可容忍丢失(如非关键的埋点日志),用 DiscardPolicy 简单丢新。
两者的共同风险是静默丢失——不抛异常、默认不打日志,出问题很难排查。所以生产环境即使要丢,也建议自定义策略在丢弃处打日志/计数,别用裸的 Discard。
五、自定义策略:落库、降级、告警
内置四种不够时,实现 RejectedExecutionHandler 接口自定义。这是生产环境最常见的做法:
public class PersistRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
// 1. 落库/写 MQ,事后补偿重试(任务不丢)
taskRepository.save(serialize(task));
// 2. 打点计数,触发告警
metrics.counter("threadpool.rejected").increment();
log.warn("线程池 {} 过载,任务已落库", executor);
}
}
常见自定义方向:落库/发 MQ 事后补偿(保证不丢,异步重试)、降级返回(返回缓存/默认值)、限流告警(记录+通知运维扩容)。核心思路是把「被拒绝」变成一个可观测、可补偿的事件,而不是简单抛异常或静默丢弃。
六、如何选:一张决策表
| 业务诉求 | 推荐策略 |
|---|---|
| 任务重要,被拒必须让调用方知道 | AbortPolicy(默认)+ 调用方降级 |
| 任务绝不能丢,能接受变慢 | CallerRunsPolicy(反压) |
| 任务绝不能丢,且要事后重试 | 自定义(落库/MQ 补偿) |
| 新数据比旧数据有价值 | DiscardOldestPolicy |
| 任务可有可无、可容忍丢失 | DiscardPolicy(建议加日志) |
一个反直觉但重要的点:拒绝策略要和队列容量、线程数一起设计。如果队列设得过大(甚至无界),拒绝策略永远不触发,等来的是 OOM 而不是「优雅拒绝」——这就是为什么要用有界队列:有界队列 + 合理 max + 匹配业务的拒绝策略,三者共同构成过载保护。
记忆钩子:「Abort 抛异常(快失败)、CallerRuns 提交者自己跑(反压不丢)、Discard 丢新、DiscardOldest 丢旧;不够就自定义落库补偿」。
七、常见误区与追问
- 误区:拒绝策略在队列满时就触发。 要「队列满 且 线程数达 maximumPoolSize」才触发;队列满但还能建非核心线程时不会拒绝。
- 误区:默认拒绝策略是丢弃任务。 默认是 AbortPolicy,抛 RejectedExecutionException,不是丢弃;丢弃是 Discard 系列。
- 误区:CallerRunsPolicy 会丢任务。 不丢,它让提交线程自己执行任务,同时形成反压减缓提交速度,是「不丢任务 + 自动限速」。
- 误区:用了拒绝策略就安全了。 若队列无界,拒绝策略根本不会触发,任务堆积到 OOM;必须配有界队列拒绝策略才生效。
- 追问:DiscardPolicy 和 DiscardOldestPolicy 都不打日志,怎么排查? 生产环境建议自定义策略在丢弃处打日志/埋点计数,或直接改用自定义 handler,避免任务静默消失难以定位。
- 追问:execute 和 submit 提交时,被拒绝的异常表现一样吗? execute 直接抛 RejectedExecutionException;submit 会把任务包成 FutureTask,异常在调用 Future.get() 时以 ExecutionException 形式暴露。
- 追问:CallerRunsPolicy 有什么风险? 提交线程(如 Web 请求线程)会被占用去执行任务,若任务耗时长会拖慢响应、降低吞吐,极端情况下提交线程被拖垮,需评估任务耗时。
八、加强记忆
拒绝策略是线程池过载的最后一道关卡——只在「队列满 且 线程数达 maximumPoolSize」时触发。四种内置策略对应四种过载态度:AbortPolicy(默认)抛 RejectedExecutionException,快速失败让调用方感知;CallerRunsPolicy 让提交线程自己执行任务,既不丢任务又形成「反压」自动限速,最巧妙;DiscardPolicy 静默丢新任务、DiscardOldestPolicy 丢队列最老的再收新的,两者按「新旧数据谁更有价值」二选一,但都有静默丢失的排查风险。都不满足就实现 RejectedExecutionHandler 自定义(落库/MQ 补偿、降级、告警),把「被拒」变成可观测可补偿的事件。关键前提:拒绝策略必须配有界队列才有意义,无界队列会让它永不触发、直接 OOM。一句话「Abort 快失败、CallerRuns 反压不丢、Discard 丢新、DiscardOldest 丢旧、自定义做补偿,前提是队列有界」。