了解线程池的工作原理吗?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
回答话术
线程池通常指 JDK 中 Executor
接口中最通用也是最常用的实现类 ThreadPoolExecutor
,它基于生产者与消费者模型实现,从功能上可以分为三个部分:
- 线程池本体:负责维护运行状态、管理工作线程以及调度任务。
- 工作队列:即在构造函数中指定的阻塞队列,它扮演者生产者消费者模型中缓冲区的角色。工作线程将会不断的从队列中获取并执行任务。
- 工作线程:即持有
Thread
对象的内部类Worker
,当一个Wroker
被创建并启动以后,它将会不断的从工作队列中获取并执行任务,直到它因为获取任务超时、任务执行异常或线程池停机后才会终止运行。
当我们向线程池提交任务时,线程池将根据下述逻辑处理任务:
- 如果当前工作线程数小于核心线程数,则启动一个工作线程执行任务。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列未满,则将任务添加到阻塞队列。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列已满,则启动一个工作线程执行任务。
- 如果当前工作线程数已达最大值,且阻塞队列已满,则触发拒绝策略。
而当一个工作线程启动以后,它将会在一个 while 循环中重复执行下述逻辑:
- 通过
getTask
方法从工作队列中获取任务,如果拿不到任务就阻塞一段时间,直到超时或者获取到任务。如果成功获取到任务就进入下一步,否则就直接进入线程退出流程; - 调用
Worker
的lock
方法加锁,保证一个线程只被一个任务占用; - 调用
beforeExecute
回调方法,随后开始执行任务,如果在执行任务的过程中发生异常则会被捕获; - 任务执行完毕或者因为异常中断,此后调用一次
afterExecute
回调方法,然后调用unlock
方法解锁; - 如果线程是因为异常中断,那么进入线程退出流程,否则回到步骤 1 进入下一次循环。
问题详解
1. 概述
1.1. 实现体系
通常当我们提到“线程池”时,狭义上指的是 ThreadPoolExectutor
及其子类,而广义上则指整个 Executor
大家族:
Executor
:整个体系的最上级接口,定义了 execute 方法。ExecutorService
:它在Executor
接口的基础上,定义了 submit、shutdown 与 shutdownNow 等方法,完善了对 Future 接口的支持。AbstractExecutorService
:实现了ExecutorService
中关于任务提交的方法,将这部分逻辑统一为基于 execute 方法完成,使得实现类只需要关系 execute 方法的实现逻辑即可。ThreadPoolExecutor
:线程池实现类,完善了线程状态管理与任务调度等具体的逻辑,实现了上述所有的接口。
ThreadPoolExecutor
作为Executor
体系下最通用的实现基本可以满足日常的大部分需求,不过实际上也有不少定制的扩展实现,比如:
- JDK 基于
ThreadPoolExecutor
实现了ScheduledThreadPoolExecutor
用于支持任务调度。- Tomcat 基于
ThreadPoolExecutor
实现了一个同名的线程池,用于处理 Web 请求。- Spring 基于
ExecutorService
接口提供了一个ThreadPoolTaskExecutor
实现,它仍然基于内置的ThreadPoolExecutor
运行,在这个基础上提供了不少便捷的方法。
此外,关于 execute 和 submit 方法的区别,你可以参见:线程池中 execute 和 submit 方法有什么区别?
1.2. 构造函数的参数
ThreadPoolExecutor
类一共提供了四个构造方法,我们基于参数最完整构造方法了解一下线程池创建所需要的变量:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程闲置存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 创建线程使用的线程工厂
RejectedExecutionHandler handler // 拒绝策略) {
}
- 核心线程数:即长期存在的线程数,当线程池中运行线程未达到核心线程数时会优先创建新线程**。**
- 最大线程数:当核心线程已满,工作队列已满,同时线程池中线程总数未超过最大线程数,会创建非核心线程。
- 超时时间:非核心线程闲置存活时间,当非核心线程闲置的时的最大存活时间。
- 时间单位:非核心线程闲置存活时间的时间单位。
- 任务队列:当核心线程满后,任务会优先加入工作队列,等待核心线程消费。
- 线程工厂:线程池创建新线程时使用的线程工厂。
- 拒绝策略:当工作队列已满,且线程池中线程数已经达到最大线程数时,执行的兜底策略。
线程池每个参数的作用算是一个老生常谈的问题了,这里我们不过多赘述,你只需大概了解这几个参数即可,在下文我们会结合源码和具体的场景进一步的带你了解他们具体含义。
1.3. 工作线程 Worker
线程池的核心在于工作线程,在 ThreadPoolExecutor
中,每个工作线程都对应的一个内部类 Worker
,它们都存放在一个 HashSet
中:
private final HashSet<Worker> workers = new HashSet<Worker>();
Worker
类的大致结构如下:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable {
// 线程对象
final Thread thread;
// 首个执行的任务,一般执行完任务后就保持为空
Runnable firstTask;
// 该工作线程已经完成的任务数
volatile long completedTasks;
Worker(Runnable firstTask) {
// 默认状态为 -1,禁止中断直到线程启动为止
setState(-1);
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
Worker
本身实现了 Runnable
接口,当创建一个 Worker
实例时,构造函数会通过我们在创建线程池时指定的线程工厂创建一个 Thread 对象,并把当前的 Worker 对象作为一个 Runnable 绑定到线程里面。当调用它的 run
方法时,它会通过调用线程池的 runWorker
反过来启动线程,此时 Worker 就开始运行了。
Worker
类继承了 AbstractQueuedSynchronizer
,也就是我们一般说的 AQS,这意味着当我们操作 Worker 的时候,它会通过 AQS 的同步机制来保证对工作线程的访问是线程安全。比如当工作线程开始执行任务时,就会“加锁”,直到任务执行结束以后才会“解锁”。
1.4. 主锁 mainLock
在上文介绍工作线程的时候,我们会注意到,线程池直接使用一个 HashSet
来存储 Worker
示例,而 HashSet
本身却并非线程安全的,那在并发场景下要如何保证线程安全呢?
实际上,除了 workers
以外,线程池中还有大量非线程安全的变量,这里再举几个例子:
ctl
:记录线程池状态与工作线程数。largestPoolSize
/corePoolSize
:最大/核心工作线程数。completedTaskCount
:已完成任务数。keepAliveTime
:核心线程超时时间。
这些变量实际上环环相扣,因此很难通过分别将它们改为原子变量/并发容器来保证线程安全,因此 ThreadPoolExecutor
选择为整个线程池提供一把主锁 mainLock
,每次操作或读取这种全局性变量的时候,都需要获取主锁才能进行:
private final ReentrantLock mainLock = new ReentrantLock();
�比如获取当前工作线程数的时候:
public int getPoolSize() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 如果线程已经开始停机,则返回 0,否则返回工作线程数量
return runStateAtLeast(ctl.get(), TIDYING) ? 0
: workers.size();
} finally {
mainLock.unlock();
}
}
总的来说,线程池通过 mainLock 来保证全局配置的线程安全,而每个工作线程再通过 AQS 来保证工作线程自己的线程安全。
这里顺带一提,也正因为如此,当我们对线程池进行监控时,需要谨慎的选择获取状态信息方法的频率,关于这部分内容,可以参见:✅ 如何监控线程池的运行性能指标?
2. 状态控制
2.1. ctl
线程池拥有一个 AtomicInteger
类型的成员变量 ctl ,它是 control 的缩写,线程池分别通过 ctl 的高位低位来管理两部分状态信息:
- 第一部分为高 3 位,用来记录线程池当前的运行状态。
- 第二部分为低 29 位,用来记录线程池中的工作线程数。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 29(32-3)
private static final int COUNT_BITS = Integer.SIZE - 3;
// ======== 线程数相关常量 ========
// 允许的最大工作线程(2^29-1 约5亿)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// ======== 线程状态相关常量 ========
// 运行状态。线程池接受并处理新任务
private static final int RUNNING = -1 << COUNT_BITS;
// 关闭状态。线程池不能接受新任务,处理完剩余任务后关闭。调用shutdown()方法会进入该状态。
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 停止状态。线程池不能接受新任务,并且尝试中断旧任务。调用shutdownNow()方法会进入该状态。
private static final int STOP = 1 << COUNT_BITS;
// 整理状态。由关闭状态转变,线程池任务队列为空时进入该状态,会调用terminated()方法。
private static final int TIDYING = 2 << COUNT_BITS;
// 终止状态。terminated()方法执行完毕后进入该状态,线程池彻底停止。
private static final int TERMINATED = 3 << COUNT_BITS;
在实际使用时,线程池将会通过位运算从 ctl 变量中解析出所需要的部分,并且做出相应的修改。
2.2. 通过 clt 计算当前状态
在开始前,我们需要理解一下关于补码的知识:在计算机中,二进制数中的首位是符号位,即第一位为 0 时表示正数,为 1 时表示负数。当我们需要表示一个负数时,则需要对它对应的正数按位取反再 + 1(也就是先取反码再获得补码)。
举个例子,如果我们假设在代码中使用 1 字节 —— 也就是 8 bit,即 8 位 —— 来表示一个数字,那么这种情况下,1 的二进制表示方式为 0000 0001,而 -1 则为 1111 1111
现在我们对补码有了基础的了解,那么就可以尝试着理解线程池是如何通过 -1 << COUNT_BITS
这行代码来表示 RUNNING
这个状态的:
- Java 中 int 是 4 个字节,即 32 bit,按上述过程 -1 这个值转为二进制即为 1 111......1111(32 个 1);
COUNT_BITS
是 29,-1 << COUNT_BITS
这行代码表示让 -1 左移 29 位。- 我们对 -1 左移 29 位后得到了一个 32 位的 int 值,它转为二进制就是 1110...0000,即前 3 位为 1,其余 29 位都为 0。
同理,计算其他的几种状态,最终可知五种状态对应的二进制表示分别是:
状态 | 二进制 |
---|---|
RUNNING | 1110...0....00(可记为 111) |
SHUTDOWN | 0000...0....00(可记为 000) |
STOP | 0010...0....00(可记为 001) |
TIDYING | 0100...0....00(可记为 010) |
TERMINATED | 0110...0....00(可记为 011) |
有意思的地方在于,RUNNING 的符号位是 1,说明它转为十进制以后是个负数,而除它以外其他的状态的符号位都是 0,转为十进制之后都是正数,也就是说,我们可以这么认为:
小于 SHUTDOWN 的就是 RUNNING,大于 SHUTDOWN 就是停止中或者已停止。
这也是后面状态计算的一些写法的基础。比如 isRunning()
方法:
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
2.3. 通过 ctl 计算工作线程数
在代码中,我们会经常看到线程池通过这一段代码来获取状态:
// 29(32-3)
private static final int COUNT_BITS = Integer.SIZE - 3;
// 允许的最大工作线程(2^29-1 约5亿)
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static int workerCountOf(int c) {
return c & CAPACITY;
}
这个运算过程其实与上文运行状态的运算过程基本一致:
- 在 Java 中,int 值 1 的二进制表示为 0000……1,即除了最后一位为 1 以外其他都为 0;
- 然后
1 << COUNT_BITS
表示让 1 左移 29 位,即得到 0010……0,即除了第三位为 1 以外其他位都为 0; - 随后对该值再 -1,即得到 0001……1,即除了前三位为 0 以外,其余 29 位皆为 1,此时该值实际上就是一个掩码了;
- 此后,我们执行
c & CAPACITY
,实际上就会得到低 29 位的值,即当前线程中的线程数量。
这里我们举个例子,假如当前线程池处于 RUNNING 状态,且有 1 个工作线程,那么此时 ctl 值为 1110……0001,即前三位和最后一位为 1,其余位数都为 0,然后与 CAPACITY 进行与运算后,高三位全变为 0,此时 ctl 即为 0000……0001,也就是 1。
3. 线程池状态
3.1. 状态的流转
在前文,我们提到线程池通过 ctl 一共可以表示五种状态:
- RUNNING:运行状态。线程池接受并处理新任务。
- SHUTDOWN :关闭状态。线程池不能接受新任务,处理完剩余任务后关闭。调用
shutdown
方法会进入该状态。 - STOP:停止状态。线程池不能接受新任务,并且尝试中断旧任务。调用
shutdownNow
方法会进入该状态。 - TIDYING:整理状态。由关闭状态转变,线程池任务队列为空且没有任何工作线程时时进入该状态,会调用
terminated
方法。 - TERMINATED:终止状态。
terminated
方法执行完毕后进入该状态,线程池彻底停止。
它们具体的流转关系可以参考下图:
除了这种运行状态,线程池还提供了一些方法让我们可以获取其他的指标信息,关于这部分内容,可以参见:✅ 如何监控线程池的运行性能指标?
3.2. 如何触发停机?
当我们要停止运行一个线程池时,可以调用下述两个方法:
- shutdown:中断线程池,不再添加新任务,同时等待当前进行和队列中的任务完成。
- shutdownNow:立即中断线程池,不再添加新任务,同时中断所有工作中的任务,不再处理任务队列中任务。
3.2.1. shutdown
shutdown
是正常关闭,这个方法主要做了这几件事:
- 改变当前线程池状态为
SHUTDOWN
; - 将线程池中的空闲工作线程标记为中断;
- 完成上述过程后将线程池状态改为
TIDYING
; - 此后等到最后一个线程也退出后则将状态改为
TERMINATED
。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
// 加锁
mainLock.lock();
try {
checkShutdownAccess();
// 将线程池状态改为 SHUTDOWN
advanceRunState(SHUTDOWN);
// 中断线程池中的所有空闲线程
interruptIdleWorkers();
// 钩子函数,默认空实现
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
3.2.2. shutdownNow
shutdownNow
与 shutdown
流程类似,不过它是立即停机,因此在细节上又有点区别:
- 改变当前线程池状态为
STOP
; - 将线程池中的所有工作线程标记为中断;
- 将任务队列中的任务全部移除;
- 完成上述过程后将线程池状态改为
TIDYING
; - 此后等到最后一个线程也退出后则将状态改为
TERMINATED
。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 将线程池状态改为 STOP
advanceRunState(STOP);
// 中断线程池中的所有线程
interruptWorkers();
// 删除任务队列中的任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
此外,而在 addWorker
或者getTask
等处理任务的相关方法里,会针对 STOP
或更进一步的状态做区分,比如在 getTask
方法中,如果线程池进入了 STOP
或更进一步的状态,则会直接返回而不会继续申请任务。
3.3. 真正的停机
我们已经知道通过 shutdownNow
与 shutdown
可以触发停机流程,当两个方法执行完毕后,线程池将会进入 STOP
或者 SHUTDOWN
状态。
但是此时线程池并未真正的停机,真正的停机逻辑,需要等到线程通过 processWorkerExit
方法退出时,里面调用的 tryTerminate
方法:
final void tryTerminate() {
for (;;) {
int c = ctl.get();
// 如果线程池不处于预停机状态,则不进行停机
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
// 如果当前还有工作线程,则不进行停机
if (workerCountOf(c) != 0) {
interruptIdleWorkers(ONLY_ONE);
return;
}
// 线程现在处于预停机状态,尝试进行停机
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 尝试通过 CAS 将线程池状态修改为 TIDYING
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
// 尝试通过 CAS 将线程池状态修改为 TERMINATED
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// 进入下一次循环
}
}
简单的来说,由于在当我们调用了停机方法时,实际上工作线程仍然还在执行任务,我们可能并没有办法立刻终止线程的执行。
因此,每个线程执行完任务并且开始退出时,它都有可能是线程池中最后一个线程,此时它就需要承担起后续的收尾工作:
- 将线程池状态修改为
TIDYING
; - 调用
terminated
回调方法,触发自定义停机逻辑; - 将线程池状态修改为
TERMINATED
; - 唤醒通过
awaitTerminated
阻塞的外部线程。
至此,线程池就真正的停机了。
4. 任务调度
4.1. 任务的提交
当我们向线程池提交一个任务时 —— 无论是通过 execute
、submit
还是 invokeAll/invokerAny
方法 —— 最终都会走到 execute
方法上来:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果当前工作线程数小于核心线程数,则启动一个新工作线程执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果当前工作线程数大于核心线程数,则尝试将任务添加到工作队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果任务添加成功,但是发现线程池已经不运行了,则移除任务并且触发拒绝策略
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果任务添加成功,但是线程池中已经没有工作线程,则添加一个新的工作线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果当前工作线程数大于核心线程数,且工作队列已满,则启动一个新工作线程执行任务,
// 如果工作线程数已经达到最大值,则直接触发拒绝策略
else if (!addWorker(command, false))
reject(command);
}
我们可以将上述逻辑简化并梳理为下图:
当然,我们还可以进一步简化,将整段代码归类为四种分支逻辑:
- 如果当前工作线程数小于核心线程数,则启动一个工作线程执行任务。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列未满,则将任务添加到阻塞队列。
- 如果当前工作线程数大于等于核心线程数,且阻塞队列已满,则启动一个工作线程执行任务。
- 如果当前工作线程数已达最大值,且阻塞队列已满,则触发拒绝策略。
根据上述逻辑,我们不难意识到,如果阻塞队列设置的比较大而难以打满,那么线程池中的线程数就会一直与核心线程数保持一致从而匀速消费。
虽然这个做法能够避免非核心线程因超时而导致频繁的创建销毁,然而在一些场景下,我们仍然会希望线程池能够尽可能快的消费完剩余的任务,对于这种情况,我们可以参见这篇文章给出解决方案:线程池如何实现任务快速消费?
4.2. 任务执行
无论如何,当线程池通过 addWorker
方法启动了一个新的线程时,都将会创建一个上文提到的 Worker
对象并调用它的 run
方法,该方法最终会调用到线程池的 runWorker
方法:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 新创建的Worker默认state为-1,AQS的unlock方法会将其改为0,此后允许使用interruptIfStarted()方法进行中断
// 完成任务以后是否需要移除当前Worker,即当前任务是否意外退出
boolean completedAbruptly = true;
try {
// 循环获取任务
while (task != null || (task = getTask()) != null) {
// 加锁,防止 shutdown 时中断正在运行的任务
w.lock();
// 如果线程池状态为 STOP 或更后面的状态,中断线程任务
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 钩子方法,默认空实现,需要自己提供
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行任务
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 钩子方法
afterExecute(task, thrown);
}
} finally {
task = null;
// 任务执行完毕
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 当线程获取任务超时、或者线程异常退出时都会调用此方法移除当前线程,这两种情况的主要区别在于,
// 如果是异常退出,那么只要线程池还在运行状态,就会立刻启动一个新线程;
// 而如果不是异常退出,那么将会根据情况来是否要启动新线程,以保证线程池中保持足够数量的线程。
processWorkerExit(w, completedAbruptly);
}
}
简单的来说,这一段代码在一个 while 循环中做了这样一件事情:
- 通过
getTask
方法从工作队列中获取任务,如果拿不到任务就阻塞一段时间,直到超时或者获取到任务。如果成功获取到任务就进入下一步,否则就直接调用processWorkerExit
进入线程退出流程; - 调用
Worker
的lock
方法加锁,保证一个线程只被一个任务占用; - 调用
beforeExecute
回调方法,随后开始执行任务,如果在执行任务的过程中发生异常则会被捕获; - 任务执行完毕或者因为异常中断,此后调用一次
afterExecute
回调方法,然后调用unlock
方法解锁; - 如果线程是因为异常中断,那么调用
processWorkerExit
进入线程退出流程,否则回到步骤 1 进入下一次循环。
我们可以将上述逻辑简化为下图:
其中,getTask
中又涉及到线程的超时回收机制,关于这一部分逻辑,可以参见:✅ 线程池如何实现线程复用&超时回收?
getTask
时的等待超时时间即为构造函数中指定的超时时间。Worker
中的所有关于线程同步的方法,都是基于它继承的 AQS 实现的。
4.3. 任务缓冲
线程池是一个非常典型的生产者和消费者模型,在这个模型中,由于生产者生产速度与消费者消费速度可能不一致,所以不会让消费者直接消费生产者的数据,而是通过一个缓冲区来进行解耦削峰。
在线程池中这个缓冲区即为工作队列,也就是我们在线程池的构造函数中指定的阻塞队列。
JDK 默认提供了以下几种阻塞队列实现:
通过更换阻塞队列,配合调整线程池的核心线程数和最大线程数,我们就可以搭配出一些适用于特定场景的线程池,比如 JDK 默认在 Executors
提供的几种线程池:
// 固定大小的线程池,核心线程数与最大线程数一致,阻塞队列无限大小
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 核心线程数为0,最大线程数不设限的线程池,阻塞队列容量为0(同步队列)
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
关于阻塞队列的实现原理,可参见 说下阻塞队列的实现原理?
4.4. 拒绝策略
当任务队列已满,并且线程池中线程也到达最大线程数的时候,就会调用拒绝策略。也就是 reject()
方法:
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
默认支持的拒绝策略共四种:
- AbortPolicy:拒绝策略,直接抛出异常,默认策略。
- CallerRunsPolicy:调用者运行策略,用调用者所在的线程来执行任务。
- DiscardOldestPolicy:弃老策略,无声无息的丢弃阻塞队列中靠最前的任务,并执行当前任务。
- DiscardPolicy:丢弃策略,直接无声无息的丢弃任务。
当然,我们也可以实现 RejectedExecutionHandler
接口来定义自己的拒绝策略。比如,在 ✅ 如何感知线程池触发了拒绝策略?这篇文章中,我们就实现一个可以发送告警并记录拒绝次数的策略,从而为线程池的监控提供支持。