线程池的拒绝策略有哪些?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道有哪几种拒绝策略,更是想看你是否能说清楚每种策略的 具体行为差异,而不是笼统回答 "有四种"。

  2. 触发时机理解:考察你是否清楚拒绝策略什么时候才会被触发——核心线程满了、队列满了、最大线程数也满了,这个前提条件必须讲明白。

  3. 生产选型能力:能否根据实际业务场景(任务能不能丢、能不能阻塞、需不需要降级)选择合适的策略,甚至自定义策略,这是区分 "背八股文" 和 "真正用过" 的关键。

核心答案

Java 线程池提供了 4 种内置拒绝策略,全部实现自 RejectedExecutionHandler 接口:

拒绝策略行为是否抛异常适用场景
AbortPolicy(默认)直接抛出 RejectedExecutionException✅ 是关键任务,不允许丢失
CallerRunsPolicy由提交任务的线程自己执行该任务❌ 否不想丢任务,可接受限流
DiscardPolicy静默丢弃任务,什么都不做❌ 否可容忍任务丢失(如日志)
DiscardOldestPolicy丢弃队列头部最旧的任务,重新提交当前任务❌ 否优先处理最新任务

一句话总结:生产环境最常用 CallerRunsPolicy(自动限流、不丢任务),关键业务可自定义策略做持久化降级。

深度解析

一、拒绝策略的触发时机

很多面试者上来就背策略,但忽略了 什么时候才会触发 这个关键前提:

上图展示了拒绝策略的触发时机。只有同时满足以下 三个条件,才会触发拒绝策略:

  • 核心线程已满:当前运行的线程数已经达到 corePoolSize
  • 任务队列已满workQueue 已经塞不进新任务了(所以一定要用有界队列,无界队列永远不会满,拒绝策略永远不会触发)
  • 最大线程数已满:线程总数已经达到 maximumPoolSize,无法再创建新线程

关键提醒:如果你用的是 Executors.newFixedThreadPool(),它内部用的是 LinkedBlockingQueue(无界队列),那么队列永远不会满,拒绝策略永远不会触发,任务会一直堆积直到 OOM。这也是为什么生产环境必须用有界队列。

二、四种策略源码解析

每种策略的源码其实非常简单,直接看一眼就懂了:

1. AbortPolicy(默认)

public static class AbortPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // 直接抛异常,简单粗暴
        throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
    }
}
  • 这是最安全的策略,因为 你一定能感知到任务被拒绝了
  • 不做任何静默处理,抛异常让调用方自己决定怎么处理

2. CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            // 让提交任务的线程自己跑
            r.run();
        }
    }
}
  • 核心逻辑就是 r.run(),谁提交的谁自己执行
  • 这个策略非常巧妙,相当于 自动限流:提交线程忙着执行任务时,就没法继续提交新任务了,给了线程池喘息的时间

3. DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // 方法体是空的,什么都不做,任务直接丢弃
    }
}
  • 最 "佛系" 的策略,任务丢了也不告诉你
  • 生产环境一般不用,除非是日志采集、监控上报等可容忍丢失的场景

4. DiscardOldestPolicy

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            // 先把队列头部(等待最久的)任务丢弃
            e.getQueue().poll();
            // 再重新提交当前任务
            e.execute(r);
        }
    }
}
  • 丢弃队列中等待最久的任务,把 "位置" 腾出来给新任务
  • 适用于实时性要求高的场景(如实时监控数据),"最新的比最早的更重要"
  • 注意:如果搭配的是优先级队列(PriorityBlockingQueue),丢弃的是优先级最高的任务,可能和预期相反

三、自定义拒绝策略(生产进阶)

内置策略满足不了需求时,可以实现 RejectedExecutionHandler 接口自定义策略。生产中常见的做法是 持久化降级

public class DbPersistPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (!executor.isShutdown()) {
            // 1. 尝试重新入队(给一次机会)
            if (!executor.getQueue().offer(r)) {
                // 2. 入队失败,持久化到数据库或消息队列
                saveToDb(r);
                log.warn("任务被拒绝,已降级持久化到数据库");
            }
        }
    }

    private void saveToDb(Runnable task) {
        // 将任务信息序列化后存入 DB,后续由定时任务补偿执行
    }
}

// 使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4, 8, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new DbPersistPolicy()  // 自定义拒绝策略
);

生产常见的自定义策略思路

降级方案适用场景复杂度
记录日志 + 告警需要感知拒绝事件
持久化到数据库任务不能丢,可延迟执行
发送到消息队列异步解耦,削峰填谷
调用降级接口返回兜底结果(如推荐系统返回默认列表)

四、策略选型速查

上图展示了拒绝策略的选型思路,根据两个核心维度做决策:

  • 维度一:任务能不能丢? 如果不能丢,首选 CallerRunsPolicy(让提交线程自己执行,起到限流效果),或者自定义策略做持久化。如果能丢,继续看维度二。

  • 维度二:丢了要不要感知? 如果需要感知,用 AbortPolicy 抛异常,上层可以捕获做告警或重试。如果不需要感知,再看是丢弃最新任务(DiscardPolicy)还是最旧任务(DiscardOldestPolicy)。

面试高频追问

  1. 默认的拒绝策略是什么?
    • AbortPolicy,直接抛 RejectedExecutionException
  2. 你生产用的哪个策略?为什么?
    • 一般用 CallerRunsPolicy,因为大多数业务任务不能丢。这个策略还能自动限流——提交线程被占用后,提交速度自然降下来,线程池有时间消化积压任务。
  3. CallerRunsPolicy 有什么坑?
    • 如果线程池被 shutdown() 了,CallerRunsPolicy 会检查 isShutdown(),直接丢弃任务。另外如果所有提交线程都被阻塞在执行任务上,可能导致整个服务假死,需要配合超时机制使用。
  4. 如何实现一个 "重试" 的拒绝策略?
    • 可以在自定义策略中循环调用 queue.offer() 等待一小段时间,或者将任务放入临时缓冲队列,由后台线程重试提交。但不建议无限重试,要设置最大重试次数。

常见面试变体

  • "线程池满了,新提交的任务怎么处理?"
  • "AbortPolicyDiscardPolicy 的区别是什么?"
  • "如何设计一个不丢任务的线程池?"
  • "你知道哪些线程池参数配置不当导致的线上事故?"

记忆口诀

四种策略:Abort 抛异常,Caller 自己跑,Discard 静默丢,Oldest 丢最老

选型原则:不能丢就 CallerRun,能丢要感知就 Abort,不关心就 Discard,重实时就 Oldest

总结

线程池拒绝策略共 4 种,触发前提是 "核心线程满 + 队列满 + 最大线程满"。生产环境优先选择 CallerRunsPolicy(自动限流不丢任务),关键业务建议自定义策略做持久化降级。务必配合有界队列使用,否则拒绝策略永远不会触发。