先了解什么情况下会触发拒绝策略:
先看一个错误的理解(我的理解):
任务放入唯一的任务队列中,所有线程共享这个任务队列并从中拉取任务。
如果任务的产生速度过快,现有的核心线程无法忙过来,就会创建非核心线程提升处理任务的速度。任务的速度生产越快,需要的非核心线程越多。
如果任务产生的速度过快了,同时不能够再创建非核心线程了,就会启动拒绝策略。
里面理解有几个错误点:
最容易错的一点是:非核心线程不是在"核心线程忙不过来"时创建,而是在"任务队列满了、入队失败"时创建 ;拒绝也不是"不能再创建非核心线程"就触发,而是**无法入队且无法创建新线程(或线程池已关闭)**才触发。
正确流程以 ThreadPoolExecutor 为例,提交任务时大致是:
-
当前线程数 < corePoolSize
直接创建核心线程来执行这个任务,不先入队。
-
当前线程数 >= corePoolSize
尝试把任务放入
workQueue。 -
如果队列没满,入队成功
任务在队列里等空闲线程来取。此时即使核心线程都很忙,也不会创建非核心线程。
-
如果队列满了,入队失败
判断当前线程数是否 < maximumPoolSize。
-
如果小于,创建非核心线程来执行这个任务。
-
如果已经达到 maximumPoolSize,无法再创建线程,就触发拒绝策略。
-
-
拒绝策略触发条件
本质是:任务无法进入队列,且无法创建新线程执行它 。
另外,线程池已经
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。
如果任务不能丢,通常选 AbortPolicy 或 CallerRunsPolicy;如果任务可丢,才考虑 DiscardPolicy 或 DiscardOldestPolicy,并且必须加监控。
=