拒绝策略在工作队列已满且活跃线程数达maximumpoolsize时触发,或线程池处于shutdown/stop状态时立即触发;四种内置策略各具业务语义:abortpolicy抛异常适用于强一致性场景,callerrunspolicy由调用线程执行实现背压,discardpolicy静默丢弃适合可丢失数据,discardoldestpolicy丢弃队列最老任务以保障时效性。

拒绝策略到底在什么时候起作用
线程池不是“来者不拒”的容器,它只在特定条件下才触发拒绝逻辑。关键条件有两个:工作队列已满,且当前活跃线程数已达 maximumPoolSize。比如配置了 core=4、max=8、队列容量=10 的线程池,当已有 8 个线程全在运行,且队列里塞满 10 个待处理任务时,第 19 个提交的任务就会被拒绝。
还有一个容易被忽略的触发点:线程池处于 SHUTDOWN 或 STOP 状态后,任何新任务都会直接走拒绝流程,不管队列是否空、线程是否闲着。所以 shutdown() 后仍继续 submit(),就等于主动调用拒绝策略。
四种内置策略怎么选才不踩坑
每种策略不是功能差异,而是业务语义差异:
- AbortPolicy(默认):抛异常,适合支付、订单等强一致性场景。它不掩盖问题,让调用方立刻感知失败,便于做重试、降级或写入死信队列。但必须配套 try-catch 和监控告警,否则异常未捕获会崩掉上层线程。
- CallerRunsPolicy:由提交线程自己执行任务。本质是“背压”——让上游慢下来。适用于日志上报、指标采集等非核心路径。慎用于 Web 容器线程(如 Tomcat 的 worker 线程),否则会拖慢整个 HTTP 响应。
- DiscardPolicy:静默丢弃。开销最小,适合监控埋点、用户行为快照等可丢失数据。注意它不打日志,生产环境建议包装一层,至少记录丢弃量和时间戳,否则问题无法追溯。
- DiscardOldestPolicy:丢队列最老任务,再尝试提交新任务。适合行情推送、实时弹幕等“宁要最新、不要最旧”的场景。但要注意:若队列中存在长耗时任务,丢弃它可能比丢弃新任务代价更大。
自定义策略不能只顾功能,还要防 GC
很多自定义拒绝策略在 rejectedExecution() 方法里创建对象——比如 new LogEvent()、new RetryTask()、JSON 序列化字符串。这些对象如果生命周期稍长,就会从 Eden 区晋升到 Survivor,再进入老年代。高频拒绝时,大量短命对象涌入老年代,极易触发 Full GC,造成服务卡顿甚至雪崩。
安全做法包括:
- 拒绝逻辑尽量无状态、无对象分配。例如只调用原子计数器 incr()、发 UDP 告警,避免 new、toString()、JSON.toJSONString() 等操作。
- 如需持久化,改用对象池复用(如 Apache Commons Pool)或预分配缓存对象。
- 对日志记录做采样控制,比如每分钟最多打印 10 条拒绝日志,其余仅统计指标。
- 上线前务必压测:模拟持续拒绝,观察 GC 日志中老年代增长速率与 Full GC 频次。
组合式策略才是高可用系统的标配
单一策略很难覆盖复杂业务分层。大厂常见的是三级线程池+对应拒绝策略:
- 快速通道(小核心、无队列):用 SynchronousQueue + AbortPolicy,保证核心交易不排队、不堆积;
- 缓冲通道(大队列、中等线程):用 LinkedBlockingQueue + DiscardOldestPolicy,扛住瞬时毛刺;
- 补偿通道(小线程、优先级队列):用 PriorityBlockingQueue + 自定义策略,将拒绝任务落库或发 Kafka,异步重试。
更进一步,可封装 TieredRejectedPolicy,按顺序尝试多个 handler,一个失败自动 fallback 到下一个,提升策略鲁棒性。但注意 fallback 不应层层嵌套,避免阻塞主线程。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











