线程池任务溢出并非系统故障,而是精心设计的压力预警机制。当两个关键条件同时满足时,拒绝策略才会被触发:工作队列已满(且为有界队列),并且当前线程数已达到设定的最大线程数上限。此时,新提交的任务不会进入队列等待,也不会自动重试,而是交由预设的拒绝处理器执行特定逻辑。选择不当的拒绝策略,轻则导致任务丢失,重则可能引发调用方服务雪崩。

AbortPolicy:默认策略不等于安全,它是“快速失败”的警报器
作为线程池的默认拒绝策略,AbortPolicy 会直接抛出 RejectedExecutionException 运行时异常。它不进行任何任务处理、补偿或静默丢弃,其核心设计理念是将系统压力明确地暴露给上层调用方,从而迫使开发者必须正视并处理资源过载问题。
- 适用场景:适用于核心业务路径上的任务,例如支付结果回调、订单创建等关键操作。前提是调用方已具备完善的异常捕获机制、全链路日志记录以及实时监控告警体系。
- 必须配套的保障措施:捕获异常时,务必记录被拒任务的唯一标识和业务类型;将拒绝事件作为关键指标上报至监控系统;同时,必须配置好服务熔断或业务降级等兜底方案,防止异常扩散。
- 常见错误用法:在Web接口中直接抛出该异常而未做捕获处理,导致用户端收到HTTP 500错误,同时后台缺乏有效日志来定位具体是哪个请求被拒绝,使得问题排查异常困难。
CallerRunsPolicy:看似“不丢失任务”,实则将压力反向传导给调用方
该策略会让提交任务的调用者线程(例如Tomcat的HTTP工作线程)亲自同步执行被拒绝的任务。它既不创建新线程,也不丢弃任务,听起来非常理想。但其代价是会严重拖慢调用方自身的处理速度,可能引发连锁反应。
- 适用前提:调用方自身的吞吐量可控且压力不大,例如后台定时调度任务;或者已经对任务提交频率做了严格的限流控制。
- 高风险场景:试想,一个处理HTTP请求的线程被占用去执行一个耗时的计算任务,这将直接导致接口响应时间激增,打满Web容器的连接池,最终可能引发上游服务的超时雪崩。
- 建议的防护措施:在提交任务前,可先判断线程池状态,例如使用
if (!executor.isShutdown()),避免在线程池关闭后仍强行提交任务,造成不必要的阻塞。
DiscardPolicy 与 DiscardOldestPolicy:静默丢弃任务,但丢弃逻辑与后果截然不同
这两种策略都不会抛出异常,保持了表面的“平静”,但它们在丢弃对象的选择和带来的潜在副作用上存在显著差异。
- DiscardPolicy:直接丢弃当前新提交的任务,不留任何日志或痕迹。这种策略仅适用于完全可丢弃的非关键数据,例如客户端的辅助性埋点上报、无关紧要的心跳检测等。
- DiscardOldestPolicy:它会从任务队列的头部移除一个最早进入的任务(无论其重要性如何),然后尝试执行当前新提交的任务。如果队列持续满载,可能导致高频到达的新任务不断挤掉队列中的老任务,造成一些关键性任务在队尾“饿死”,永远得不到执行。
- 共有的风险:线上系统无法感知任务被丢弃的行为。如果没有对策略进行包装并加入带有限流功能的日志记录,整个丢弃过程就如同在“盲操作”,为系统稳定性埋下隐患。
自定义拒绝策略:自由度极高,但需警惕三个常见陷阱
通过实现 RejectedExecutionHandler 接口来自定义策略看似简单,但在实际编码中极易引入新的性能瓶颈或逻辑缺陷。
- 严禁在拒绝处理中执行阻塞操作:绝对不要在
rejectedExecution()方法内部调用executor.submit()或任何可能阻塞的操作(例如未设置超时的远程服务调用、数据库写入),否则极易导致线程死锁或资源耗尽。 - 补偿逻辑必须具备健壮性:如果设计异步补偿机制(如将任务发送到消息队列进行重试),那么补偿逻辑本身必须包含超时控制、重试次数上限以及最终失败时的降级策略(例如降级为写入本地文件暂存)。
- 避免引发日志风暴:切忌在拒绝时高频打印完整的异常堆栈信息。建议改用警告级别的日志,并配合限流功能,防止在流量洪峰时瞬间打爆日志系统,影响问题定位。
