Skip to content

Java 线程池有哪些拒绝策略? ​

  • 标签:Java并发、Java

🏛 业务架构师 ​

四种拒绝策略,我按**"业务希望过载时发生什么"来定价**:

  • AbortPolicy(默认,抛异常):快速失败 + 告警。适合任务不能丢也不能等的场景——交易处理线程池满了,宁可立刻报错让用户重试,也不能堆着慢慢处理造成超时假象。配套要求:异常要有监控告警,用户侧有友好提示("当前繁忙");
  • CallerRunsPolicy(调用者执行):提交线程自己跑任务——天然反压:生产者被拖慢,从源头降速。适合削峰场景(日志上报、消息消费),代价是调用方线程被占用,Web 场景会拖慢接口响应,慎用于在线链路;
  • DiscardPolicy(静默丢弃):丢任务且不留痕。只适合完全可丢的数据(指标采样、调试日志)——裸用这个策略几乎是事故,丢了也没人知道;
  • DiscardOldestPolicy(丢最老的):腾位置给新任务。适合"只要最新状态"的场景——行情推送、实时监控点,旧数据过期即无价值。

我的实战组合拳(比单选更重要):

  1. 拒绝时落库/落 MQ:自定义 RejectedExecutionHandler——线程池满了把任务写到 MQ 稍后补偿,过载时的第二缓冲区;
  2. 拒绝策略与监控联动:拒绝计数是核心容量指标,拒绝次数上涨 = 容量扩容/降级的触发信号,而非普通异常噪音;
  3. 分池分策:交易池 Abort、通知池 CallerRuns、埋点池自定义丢弃+计数。

架构观点:拒绝策略本质是过载时的业务价值排序——保什么、丢什么、慢化谁。这个排序产品经理也该参与定义,过载行为是产品行为,不只是技术参数。

🏗 系统架构师 ​

四种内置拒绝策略 + 选型逻辑:

  • AbortPolicy(默认):抛 RejectedExecutionException。快速失败、问题显性化,适合任务可丢弃、上游有兜底重试的场景——异常抛出后由调用方决定重试/降级;
  • CallerRunsPolicy:让提交任务的线程自己执行。妙在自动反压:消费不动时生产者被拖慢,天然削峰不丢任务;风险是提交线程(比如 Tomcat 线程、Netty EventLoop)被阻塞——IO 线程被占住会拖垮整个服务的响应,所以我不允许在网关/Netty 链路上用它;
  • DiscardPolicy:静默丢弃,没有任何痕迹。日志埋点类可容忍,业务任务用它等于埋雷,基本禁用;
  • DiscardOldestPolicy:丢队列头部最老任务再重试提交。适合"只要最新状态"的场景(行情推送、监控指标),旧数据天然过期。

我的实践扩展:生产上通常自定义 RejectedExecutionHandler:丢弃前落库/MQ 暂存、发监控告警、按业务优先级决定去留。配套原则:

  • 队列必须有界(无界队列永远轮不到拒绝策略,先 OOM);
  • 线程池按业务隔离(支付池、通知池分开),避免低优先级任务淹没高优先级;
  • 拒绝计数是容量规划信号,频繁拒绝说明该扩容或调参了。

拒绝策略的选型本质是回答:过载时,丢任务、拖慢上游、还是抛错误,哪个业务代价最小。

💻 全栈开发专家 ​

四种内置 + 自定义:

  1. AbortPolicy(默认):抛 RejectedExecutionException。如果是 IO 线程直接 catch,任务丢失只留一条异常日志——生产上我不推荐裸用。
  2. CallerRunsPolicy:让提交任务的线程自己执行。天然反压:提交者被拖慢,上游产生速度自动降下来。代价是可能阻塞 Tomcat 线程/消息消费线程——适合"宁可慢不能丢"的场景(我用于日志/通知类线程池)。
  3. DiscardPolicy:静默丢弃,无感知。统计埋点场景可用,业务数据严禁。
  4. DiscardOldestPolicy:丢队列里最老的再重试提交,保留"最新",适合行情/监控采样类时效性数据。

我生产的自定义策略(推荐姿势):不选内置,按业务定——

java
new RejectedExecutionHandler() {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            try {
                taskBackupRepository.save(r);   // 落库/发 MQ 留待补偿
                metrics.increment("pool.rejected");
                alert.fire("线程池拒绝", e.toString());   // 告警
            } catch (Exception ex) {
                log.error("reject backup failed", ex);
            }
        }
    }
}

要点:拒绝是过载信号,必须可观测(打点+告警),静默丢等于埋雷;拒绝发生时配合动态调参(调大队列/线程数)或上游限流才是完整闭环。