很多人一提到线程池的拒绝策略,第一反应是“出问题了怎么办”。但说实话,RejectedExecutionHandler 并不应该被当作故障补救措施来理解——它本质上是一个提前设计好的压力信号。这个信号只在两个条件同时满足时才会触发:工作队列已经满了(而且得是有界队列),同时当前活跃线程数已经达到了 maximumPoolSize。换句话说,新任务既排不上队,也开不了新线程,系统直接告诉你:扛不住了,你自己看着办。
这时候,拒绝策略开始发挥作用。但选错了策略,后果很微妙:轻则任务悄悄丢失,你完全不知道;重则反向压垮调用方,引发连锁反应。下面我们就逐个拆解,看看每种策略到底适合什么场景,又藏着哪些坑。

AbortPolicy:快速失败,但必须配套可观测性
这是线程池的默认策略,逻辑很简单:直接抛出 RejectedExecutionException,不补偿,不静默。它的核心价值就在于“立刻暴露问题”,倒逼上游自己做决策。
哪些场景适合?比如支付回调、订单落库、风控同步校验这类关键路径上的任务,宁可失败重试,也不能默默丢掉。但这里有个前提:你必须捕获异常,并且记录下任务标识(比如 traceId、taskId)、任务类型和提交时间,否则你只知道“出了异常”,却不知道是哪个任务出了问题。
此外,建议打点监控指标,比如 rejected_task_count,并联动告警。特别要注意的是,在 Web 场景中,如果没做异常捕获,RejectedExecutionException 会直接返回 HTTP 500,而且没有上下文日志,定位起来非常痛苦。
CallerRunsPolicy:调用线程兜底,但风险隐蔽
看名字就知道,这个策略会让提交任务的线程(比如 Tomcat 的 worker 线程)自己去执行被拒的任务。表面上看,任务没有被丢弃,似乎很安全。但问题恰恰出在这里:它把压力静悄悄地传导回了上游。
适用场景是那些提交方本身可控的情况,比如定时调度器每秒最多 submit 3 次,即使回退执行,也不会对系统造成太大冲击。但如果是 HTTP 接口大量使用这个策略,风险就非常隐蔽了:响应延迟上升 → 连接池耗尽 → 上游超时重试 → 压力倍增,最终引发雪崩。
另外,务必在 execute 前加一个判断:if (!executor.isShutdown()),避免线程池 shutdown 后仍然强行执行任务。总的来说,高吞吐网关或 API 层不建议直接启用这个策略。
DiscardPolicy 与 DiscardOldestPolicy:静默丢弃,行为差异大
这两个策略都不抛异常,但丢法完全不同,后果也天差地别。
- DiscardPolicy:直接丢弃当前新任务,零日志,零痕迹。只适用于完全可丢的流量,比如前端埋点、非关键心跳——丢了也不影响核心业务。
- DiscardOldestPolicy:先从队列头 poll 一个最老的任务(不管它有多重要),然后尝试执行当前任务。问题在于,如果队列持续满,高频任务会反复被丢,而真正关键的任务可能卡在队尾,永远得不到执行。
这两个策略有一个共同的致命缺陷:线上完全无法感知丢弃行为。如果你没有自行包装策略并加入限流日志(比如 logger.warn("discarded: {}", taskId)),那基本等于盲调,出了事都不知道从哪查起。
自定义策略:自由度高,但三个关键约束不能破
实现 RejectedExecutionHandler 接口看起来很简单,但实际写起来很容易引入新的瓶颈。这里有几个必须遵守的约束:
- 禁止在
rejectedExecution方法中调用executor.submit()或任何可能阻塞的操作,否则可能导致死锁或线程耗尽。 - 如果要做异步补偿(比如发 Kafka、写 DB),补偿逻辑自身必须带超时和降级,否则拒绝处理反而成了系统的新瓶颈。
- 避免在拒绝逻辑中打印完整堆栈(比如
e.printStackTrace())。高频拒绝时,I/O 会直接打爆磁盘或日志系统;改用带限流的 warn 日志会更安全。
说到底,拒绝策略的选择不是简单的“哪个好”,而是“哪个更匹配你的场景和可观测性能力”。没有完美的策略,只有最适合当前系统的设计。