简单讲解线程池的4种拒绝策略

先了解什么情况下会触发拒绝策略:

先看一个错误的理解(我的理解):

任务放入唯一的任务队列中,所有线程共享这个任务队列并从中拉取任务。

如果任务的产生速度过快,现有的核心线程无法忙过来,就会创建非核心线程提升处理任务的速度。任务的速度生产越快,需要的非核心线程越多。

如果任务产生的速度过快了,同时不能够再创建非核心线程了,就会启动拒绝策略。

里面理解有几个错误点:

最容易错的一点是:非核心线程不是在"核心线程忙不过来"时创建,而是在"任务队列满了、入队失败"时创建 ;拒绝也不是"不能再创建非核心线程"就触发,而是**无法入队且无法创建新线程(或线程池已关闭)**才触发。

正确流程以 ThreadPoolExecutor 为例,提交任务时大致是:

  1. 当前线程数 < corePoolSize

    直接创建核心线程来执行这个任务,不先入队。

  2. 当前线程数 >= corePoolSize

    尝试把任务放入 workQueue

  3. 如果队列没满,入队成功

    任务在队列里等空闲线程来取。此时即使核心线程都很忙,也不会创建非核心线程

  4. 如果队列满了,入队失败

    判断当前线程数是否 < maximumPoolSize。

    • 如果小于,创建非核心线程来执行这个任务。

    • 如果已经达到 maximumPoolSize,无法再创建线程,就触发拒绝策略。

  5. 拒绝策略触发条件

    本质是:任务无法进入队列,且无法创建新线程执行它

    另外,线程池已经 shutdown / stop 等非 RUNNING 状态时,提交任务也会走拒绝策略。

说法可以这样纠正:

  • "任务放入唯一的任务队列中,所有线程共享这个队列并从中拉取任务"

    基本对,但不完整 。工作线程确实共享 workQueue 并从中取任务,但新提交的任务不一定先入队。前 corePoolSize 个任务会直接创建核心线程执行;队列满后创建的非核心线程也会直接执行新任务,之后才去队列取任务。

  • "核心线程忙不过来,就创建非核心线程"

    不对。核心线程忙但队列没满时,任务会先排队,不会创建非核心线程。

  • "任务产生越快,需要非核心线程越多"

    不准确 。是否创建非核心线程取决于队列是否满,而不是生产速度本身。

    如果队列很大或无界,任务再多也只是堆积在队列里,不会创建非核心线程,甚至可能 OOM。

    如果队列很小或用 SynchronousQueue,则更容易创建非核心线程直到 maximumPoolSize

  • "不能再创建非核心线程了,就启动拒绝策略"

    还缺一个条件:队列也必须已经满/无法入队

    如果不能再创建非核心线程,但队列还能放任务,任务仍然会入队,不会拒绝。

拒绝策略的种类

  • JDK 自带的 ThreadPoolExecutor 拒绝策略 ,一共有 4 种 ;如果算自定义实现 RejectedExecutionHandler,那数量是无限的。

    策略 行为 适用场景 主要风险
    AbortPolicy 直接抛 RejectedExecutionException 不能丢任务,必须让上层感知失败,快速失败、报警、重试、降级 可能把异常抛给调用方,影响主流程
    CallerRunsPolicy 如果线程池未关闭,由提交任务的线程自己执行该任务 不想丢任务,同时希望形成背压,让上游慢下来 会阻塞调用线程,可能拖慢上游;线程池关闭时会丢弃
    DiscardPolicy 静默丢弃新任务,不抛异常 任务无关紧要、可丢失,如日志、监控采样 任务无声消失,容易掩盖问题,必须配监控
    DiscardOldestPolicy 如果线程池未关闭,丢掉队列中"最旧/队头"的任务,再尝试提交当前任务 新任务比旧任务更重要,如实时状态刷新、缓存更新 可能丢掉重要任务;对优先级队列可能丢掉优先级高的任务

    默认是 AbortPolicy

    为什么会有这些分类?核心原因是:线程池过载时,"拒绝"并不是一种单一语义,不同业务对过载的处理要求完全不同。

    可以从几个维度理解:

  • 任务能不能丢?

    • 不能丢:用 AbortPolicy 让上层知道失败,或者用 CallerRunsPolicy 让调用者自己扛。

    • 可以丢:用 DiscardPolicy 静默丢弃。

    • 旧任务可以丢、新任务不能丢:用 DiscardOldestPolicy

  • 失败要不要暴露?

    • 要暴露、要报警、要重试:AbortPolicy

    • 不想影响调用方,悄悄降级:DiscardPolicy

    • 想通过阻塞提交者来自然限流:CallerRunsPolicy

  • 谁来回退压力?

    • CallerRunsPolicy 是把压力回退给提交任务的线程,形成背压。上游提交越快,自己越容易被拖慢,从而减少任务产生速度。

    • AbortPolicy 是把压力变成异常,让上层决定怎么办。

    • Discard* 是把压力直接丢掉,保护线程池本身,但业务可能丢数据。

  • 保护目标不同?

    • 保护系统不崩:可能选择丢弃。

    • 保护数据不丢:可能选择抛异常或调用者执行。

    • 保护最新状态:可能选择丢弃旧任务。

    • 保护上游稳定:可能选择背压。

  • 框架需要可插拔。

    Java 提供了 RejectedExecutionHandler 接口,四种内置策略只是默认实现。实际业务可以自定义,比如:

    • 拒绝时写数据库、发 MQ、打日志、报警;

    • 阻塞式 put 回队列,做更强制背压;

    • 按任务优先级丢弃;

    • 降级到本地缓存或备用线程池。

1. AbortPolicy

默认策略。

复制代码
throw new RejectedExecutionException("Task ... rejected from ...");

反应:

  • 当前任务不会执行

  • 不会入队。

  • 提交任务的线程会收到 RejectedExecutionException

  • 线程池和队列里的其他任务不受影响。

  • 适合"不能丢任务,必须让上层知道失败"的场景。

所以它的反应是:快速失败,抛异常。

2. CallerRunsPolicy

源码逻辑大致是:

复制代码
if (!executor.isShutdown()) {
    r.run();
}

反应:

  • 如果线程池还没关闭,提交任务的线程自己直接执行这个任务

  • 当前任务不会丢,但也不会进入线程池队列。

  • 调用者线程会被阻塞,直到任务执行完。

  • 这等于把过载压力回退给上游,形成一种背压。

  • 如果线程池已经关闭,则什么都不做,当前任务被静默丢弃。

所以它的反应是:谁提交,谁执行;上游变慢,系统减压。

3. DiscardPolicy

源码是空实现:

复制代码
// 什么都不做

反应:

  • 当前任务被静默丢弃

  • 不执行。

  • 不入队。

  • 不抛异常。

  • 调用者通常完全不知道任务被丢了。

  • 线程池和队列里的其他任务不受影响。

所以它的反应是:悄悄扔掉,不报错。

风险是:任务无声消失,容易掩盖问题,必须配合监控。

4. DiscardOldestPolicy

源码逻辑大致是:

复制代码
if (!executor.isShutdown()) {
    executor.getQueue().poll(); // 丢掉队头任务
    executor.execute(r);        // 重新提交当前任务
}

反应:

  • 如果线程池还没关闭:

    • 先丢掉任务队列头部的"最旧"任务。

    • 然后重新提交当前任务。

    • 当前任务可能因此成功入队或执行。

  • 如果重新提交时又失败,可能会再次触发拒绝策略,继续丢队头。

  • 如果线程池已经关闭,则什么都不做,当前任务被静默丢弃。

  • 不抛异常。

  • 注意:"最旧"是队列头部,具体含义取决于队列实现。

    比如 LinkedBlockingQueue 是等待最久的;PriorityBlockingQueue 的队头可能是优先级最高的,不一定最旧。

所以它的反应是:丢旧任务,保当前任务。

一句话对比

  • AbortPolicy:抛异常,任务不执行。

  • CallerRunsPolicy:调用者自己执行,任务不丢,但会阻塞上游。

  • DiscardPolicy:静默丢弃当前任务。

  • DiscardOldestPolicy:丢弃队列里最旧的任务,再尝试提交当前任务。

默认是 AbortPolicy

如果任务不能丢,通常选 AbortPolicyCallerRunsPolicy;如果任务可丢,才考虑 DiscardPolicyDiscardOldestPolicy,并且必须加监控。

=

相关推荐
白远山1 小时前
24小时自助健身房系统开发实战:从需求分析到完整指南
java·开发语言·数据库·数据挖掘·需求分析
暮雨哀尘1 小时前
JAVA基础练习(四):SpringBoot 实现简易登录功能
java·spring boot·eclipse·maven
光依旧2 小时前
MCP实战手记系列(二):跑通第一个 MCP Server(文末附github源码链接)
java·spring ai·mcp·ai 开发·源码实战
白远山2 小时前
上海24小时自助健身房系统软件开发实战指南:从需求到部署
java·架构·uni-app·需求分析
MayBaymax3 小时前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程
斑鸠喳喳3 小时前
可重入锁 ReentrantLock
java·源码
IT枫斗者枫哥3 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java
devpotato3 小时前
HashMap 扩容机制:从源码细节到工程实践
java