ThreadPoolExecutor-初识
初识
📙 NOTE
《初识》
正所谓“先有孙子后有爷”,想研究threadPoolExecutor 我们先拔一拔它的上层。
可以看出最上级的接口是Executor, AutoCloseable,分别提供execute和close方法,ExecutorService继承于这两个接口,我们常用的submit就是这个类所提供的,抽象方法AbstractExecutorService去实现ExecutorService, 而我们的主角ThreadPoolExecutor是继承于AbstractExecutorService。
了解祖孙三代后,需要思考几个问题:
- 既然叫线程池,他是如何管理线程的?如何创建?
- 八股文说
线程池是一种基于池化思想管理线程的工具,使用线程池可以减少创建销毁线程的开销,避免线程过多导致系统资源耗尽。充分利用池内计算资源,等待分配并发执行任务,提高系统性能和响应能力。
在业务系统开发过程中,线程池有两个常见的应用场景,分别是:快速响应用户请求和快速处理批量任务。
那么线程池如何做到减少创建销毁线程?是如何保存线程的?
- 刚才看到的执行方法execute和submit有什么区别?
- 线程池线程安全是如何实现的?
Tips: 任何未知的东西都需要保持疑问,带着问题去探索成功率会更高。
交织
带着上面的问题,默默的打开了源码。
核心成员
找到最全的一个构造器 分析一下
/**
* corePoolSize:在线程池中的线程数量,即使他们处于空闲的状态
* maximumPoolSize:池中允许的最大线程数
* keepAliveTime:当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间。
* unit:keepAliveTime的时间单位。
* workQueue:用于在执行任务之前保存任务的队列。此队列将仅保存可运行提交的任务执行方法。
* threadFactory:执行器创建新线程时要使用的工厂
* handler:由于达到线程边界和队列容量而阻塞执行时要使用的处理程序
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
String name = Objects.toIdentityString(this);
this.container = SharedThreadContainer.create(name);
}
2.1 corePoolSize
注解描述的是,当线程池接收新的任务进来,即使存在空闲的线程也需要创建新的线程,除非线程数量达到了corePoolSize。线程池默认的线程数量是空的,而是等任务进来才会去创建线程。但内置了prestartAllCoreThreads()和prestartCoreThread()两个预处理方法,即使没有任何也会预先创建线程数量为corePoolSize,超过后方到阻塞队列里。
2.2 maximumPoolSize
注释描述为:池中允许的最大线程数。会有人问刚才不是说超过corePoolSize就放到了队列中了吗,怎么又出现最大的概念? 这里关系可以理解为:一个工厂5个工人,一个工人只能同时做一个鸡哥牌玩偶,玩偶供不应求 一天需要10个玩偶,5个工人在做 另外5个就排队呗,但突然增加到13个了。这是老板决定临时雇3个工人但最多只能3个,这里3+5就是最大线程数。过段时间不忙了 5个工人足以支撑的时候3个工人就辞退了 这里的时间就是下面说到的线程存活时间
2.3 keepAliveTime
当线程数大于核心时,这是多余的空闲线程在终止之前等待新任务的最长时间。即:外包人员辞退时间
2.4 workQueue
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列
2.5 handler
- CallerRunsPolicy:拒绝任务的处理程序,它直接在execute方法的调用线程中运行拒绝的任务,除非执行器已关闭,在这种情况下,任务将被丢弃。
- AbortPolicy:引发RejectedExecutionException的被拒绝任务的处理程序。这是ThreadPoolExecutor和ScheduledThreadPoolExecutor的默认处理程序
- DiscardPolicy:直接丢弃,其他啥都没有
- DiscardOldestPolicy:当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入。
2.6 基本属性
// 初始化线程池控制变量,初始状态为RUNNING且没有工作线程
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 计算工作线程计数位数,共31位
private static final int COUNT_BITS = Integer.SIZE - 3;
// 工作线程计数掩码,用于获取ctl中的工作线程数
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 状态存储在高3位中
// 线程池运行状态,表示线程池正在运行
private static final int RUNNING = -1 << COUNT_BITS;
// 线程池关闭状态,表示不再接受新任务但未执行完的任务继续执行
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 线程池停止状态,表示不再接受新任务并且立即中断所有正在执行的任务
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值
// 获取ctl中的运行状态
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
// 获取ctl中的工作线程数
private static int workerCountOf(int c) { return c & COUNT_MASK; }
// 根据运行状态和工作线程数创建ctl值
private static int ctlOf(int rs, int wc) { return rs | wc; }
// 不需要解包ctl的位字段访问器
// 判断运行状态是否小于给定状态
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
// 判断运行状态是否大于等于给定状态
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
// 判断线程池是否处于运行状态
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
这里ctl 变量是一个 AtomicInteger 类型的变量,它用于同时保存线程池的状态和当前活动的工作线程数。具体来说:
高3位:表示线程池的状态。
低31位:表示当前活跃的工作线程数。
可以理解为他同时维护了线程池的状态和工作线程数量,至于具体怎么算的不重要,后面还会再次聊这个。
这几个属性和方法在后面会经常遇见,更是保证线程安全的手段。
工作流程
说完基本属性聊一下线程池工作流程:
- 如果运行的线程数少于corePoolSize的时候,直接创建新线程来处理任务。即使线程池中的其他任务是空闲的(会默认先把corePoolSize填充满,保证核心线程数)
- 当corePoolSize满了的时候,新任务提交时,会被推入任务队列进行等待。完成任务的核心线程会从任务队列中进行领取。
- 当任务队列被任务推满,并且继续被推新任务时,会创建非核心线程。非核心线程的数量不能超过maxinumPoolSize,被创建后立即领取推入的新任务进行执行
- 非核心线程完成新任务,处于空闲时,会从任务队列中领取任务进行执行。如果没领取到,则会一直处于空闲状态,直到keepAliveTime耗尽,该非核心线程会被销毁。
- 如果corePoolSize与maxinumPollSize相同的话,那么创建的线程池的大小是固定的,此时如果有新任务提交,而且workQueue还没满,就把请求放到workQueue中,等待有空闲的线程去workQueue中取出任务后在执行(如果corePoolSize与maxinumPollSize相同,说明不允许在线程池中创建新的线程了,让任务在队列中等待)
- 如果运行的线程数量大于maxinumPollSize,如果workQueue也已经满了,会根据指定的拒绝策略来处理该任务
判断顺序:corePoolSize -> workQueue -> maxinumPoolSize
这个做个小测试
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
200, // 非核心线程数存活时间数
TimeUnit.MILLISECONDS, // 非核心线程数存活时间单位
new LinkedBlockingQueue<>(5), // 阻塞队列
new ThreadFactoryBuilder().setNamePrefix("测试").build(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交20个任务
for (int i = 0; i < 20; i++) {
try {
MyTask myTask = new MyTask(i);
executor.execute(myTask);
printThreadPoolStatus(executor);
Thread.sleep(10); // 等待10ms
} catch (Exception e) {
e.printStackTrace();
}
}
// 关闭线程池
executor.shutdown();
System.out.println("-----------------over---------------------");
printThreadPoolStatus(executor);
}
private static void printThreadPoolStatus(ThreadPoolExecutor executor) {
System.out.println("线程池中线程数目:" + executor.getPoolSize() +
",队列中等待执行的任务数目:" + executor.getQueue().size() +
",已执行完成的任务数目:" + executor.getCompletedTaskCount());
System.out.println("--------------------------------------------------");
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task-" + taskNum);
try {
Thread.sleep(10000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("任务中断 Task:" + taskNum + ", 线程:" + Thread.currentThread().getName());
}
System.out.println("task-" + taskNum + "执行完毕");
}
}
看看结果:
自己分析吧您。
execute
/**
* 执行给定的任务,该任务可能在一个新线程或现有的线程池线程中执行。
*
* 如果任务无法被提交执行,可能是由于线程池已被关闭或其容量已达到上限,
* 则任务将由当前的{@link RejectedExecutionHandler}处理。
*
* @param command 要执行的任务
* @throws RejectedExecutionException 如果任务无法被接受执行,则根据
* {@code RejectedExecutionHandler}的策略抛出此异常
* @throws NullPointerException 如果 {@code command} 为 null
*/
public void execute(Runnable command) {
// 检查任务是否为 null
if (command == null)
throw new NullPointerException();
/*
* 执行过程分为三个步骤:
*
* 1. 如果运行中的线程数少于核心线程数,尝试启动一个新的线程来执行任务。
* 调用 addWorker 方法原子地检查 runState 和 workerCount,以防止不必要的线程创建。
*
* 2. 如果任务可以成功入队,仍然需要再次检查是否应该添加新的线程(因为之前的线程可能已经死亡)
* 或者线程池是否已经关闭。如果线程池已关闭,则回滚入队操作;如果没有线程,则启动新线程。
*
* 3. 如果无法将任务入队,则尝试添加新线程。如果失败,则表示线程池已关闭或已满,拒绝任务。
*/
// 获取当前控制变量的值
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);
}
execute方法的逻辑并不复杂:
- 当前运行的线程数小于核心线程数,则调用addworker添加新线程执行任务
- 检查线程池是否处于running状态,如果任务可以成功入队,仍然需要再次检查是否应该添加新的线程(因为之前的线程可能已经死亡)或者线程池是否已经关闭。如果线程池已关闭,则回滚入队操作;如果没有线程,则启动新线程。
- 如果无法将任务入队,则尝试添加新线程。如果失败,则表示线程池已关闭或已满,拒绝任务。
这里出现了几个重要的方法,isRunning()判断线程池是否运行、workQueue.offer()加入阻塞队列、remove()会滚移除任务、reject()拒绝任务以及更核心的addWorker()添加工人方法。后面会在聊
通过上面这一小段代码,我们就已经完整地看到了通过一个 ctl 变量进行全局状态控制,从而保证了线程安全性。整个框架并没有使用锁,但是却是线程安全的。
2.1 submit
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
submit本身是父类继承过来的,可以看到有三个重载方法,Runnable为无参数无返回、Callable为无参数有返回。
三个方法本质就是封装任务和返回结果为 RunnableFuture, 统一交由具体的子类执行,然后调用execute方法。
future类后面单独聊
2.2 invoke
worker
无论是execute还是submit,真正干活的是worker。你不干我不干老板何时才能提bwm
2.1 worker
/**
* Worker 类主要用于维护正在运行任务的线程的中断控制状态以及其他一些辅助信息。
* 这个类扩展了 AbstractQueuedSynchronizer,简化了每次任务执行时锁的获取和释放。
* 这样可以防止中断原本用于唤醒等待任务的线程却中断了正在运行的任务。
* 实现了一个简单的非重入互斥锁而不是使用 ReentrantLock,因为我们不希望任务在调用
* 线程池控制方法(如 setCorePoolSize)时能够重新获取锁。
* 此外,为了抑制线程实际开始运行任务前的中断,我们将锁状态初始化为负值,并在启动时清除(在 runWorker 中)。
*/
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* 该类永远不会被序列化,但我们提供一个 serialVersionUID 来抑制 javac 警告。
*/
private static final long serialVersionUID = 6138294804551838833L;
/** 当前线程正在运行的线程。如果工厂创建失败,则为 null。 */
@SuppressWarnings("serial") // 不太可能被序列化
final Thread thread;
/** 初始任务。可能为 null。 */
@SuppressWarnings("serial") // 未静态类型化为 Serializable
Runnable firstTask;
/** 每个线程的任务计数器 */
volatile long completedTasks;
// TODO: 切换到 AbstractQueuedLongSynchronizer 并将 completedTasks 移动到锁字中。
/**
* 使用给定的第一个任务和 ThreadFactory 创建线程。
* @param firstTask 第一个任务(如果没有则为 null)
*/
Worker(Runnable firstTask) {
setState(-1); // 抑制直到 runWorker 开始前的中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** 委托主要的运行循环到外部的 runWorker 方法。 */
public void run() {
runWorker(this);
}
// 锁方法
//
// 值 0 表示未锁定状态。
// 值 1 表示锁定状态。
protected boolean isHeldExclusively() {
return getState() != 0; // 如果状态不为 0,则表示已锁定
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) { // 尝试将状态从 0 设置为 1
setExclusiveOwnerThread(Thread.currentThread()); // 设置当前线程为独占所有者
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null); // 清除独占所有者
setState(0); // 将状态设置为 0
return true;
}
public void lock() { // 锁定
acquire(1);
}
public boolean tryLock() { // 尝试锁定
return tryAcquire(1);
}
public void unlock() { // 解锁
release(1);
}
public boolean isLocked() { // 检查是否锁定
return isHeldExclusively();
}
/**
* 如果线程已启动且未被中断,则中断线程。
*/
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt(); // 中断线程
} catch (SecurityException ignore) {
// 忽略安全异常
}
}
}
}
这个类有点多我们一点点分析,首先他继承于aqs,经常背八股文应该知道使用aqs简化了每次任务执行时锁的获取和释放。(关于aqs可以单独看这篇文章[]{}。
-
基本属性:thread运行的线程、firstTask 初始任务、completedTasks程序计数器。构造器在初始化的时候设置aqs状态为-1
setState(-1)
,同时使用ThreadFactory 去创建线程。 -
基本方法:run()委托主要的运行循环到外部的 runWorker 方法;interruptIfStarted()如果线程已启动且未被中断,则中断线程。
-
锁方法:isHeldExclusively boolean值:表示未锁定状态。值 1 表示锁定状态。
tryAcquire() 原子操作设置当前线程
tryRelease 原子操作 清除线程占有
lock 枷锁
tryLock 尝试枷锁
unlock 释放锁
isLocked 是否锁定
到这里worker类属性方法基本理清了,但是这个类到底是干嘛用的 还是有点模糊吧。worker类只提供了一个功能性方法就是run 也就是执行方法 参数为Runnable, 也就是可以理解为worker是一个维护处理一个线程和一个任务的类。
2.2 addWorker
/**
检查是否可以根据当前池状态和给定的绑定 (核心或最大值) 添加新的工作线程。
如果是这样,则相应地调整工作线程计数,如果可能,将创建并启动新的工作线程,并将firstTask作为其第一个任务运行。
如果池已停止或有资格关闭,则此方法返回false。如果线程工厂在询问时无法创建线程,它也会返回false。
如果线程创建失败,要么是由于线程工厂返回null,要么是由于异常 (通常是thread. start中的OutOfMemoryError ()), 我们会干净地回滚。
firstTask -新线程应首先运行的任务 (如果没有,则为null)。
创建worker的初始第一个任务 (在方法execute() 中),以便在少于corePoolSize线程 (在这种情况下,我们总是启动一个) 或队列已满 (在这种情况下,我们必须绕过队列) 时绕过队列。
最初空闲的线程通常通过prestartCoreThread创建或替换其他垂死的工人。
core -如果为true,则使用corePoolSize作为绑定,否则使用maximumPoolSize。(这里使用布尔指示器而不是一个值,以确保在检查其他池状态后读取新鲜值)。
退货:
如果成功,则为true
**/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get(); ; ) {
// Check if queue empty only if necessary.
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty())) {
// 如果线程池处于关闭状态或者停止状态,或者队列为空,则返回 false
return false;
}
for (;;) {
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) {
// 如果当前活动线程数已经达到核心线程数或最大线程数,则返回 false
return false;
}
if (compareAndIncrementWorkerCount(c)) {
// 尝试增加一个线程计数,如果成功则跳出循环
break retry;
}
c = ctl.get(); // 重新读取 ctl
if (runStateAtLeast(c, SHUTDOWN)) {
// 如果线程池处于关闭状态,则继续重试
continue retry;
}
// 否则 CAS 失败是由于 workerCount 改变,重试内循环
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 再次检查状态,确保线程工厂没有失败并且线程池没有关闭
int c = ctl.get();
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
// 如果线程池正在运行,或者状态小于 STOP 且没有初始任务
if (t.getState() != Thread.State.NEW) {
// 如果线程状态不是 NEW,则抛出异常
throw new IllegalThreadStateException();
}
workers.add(w); // 添加 Worker 到集合中
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize) {
// 更新最大线程数
largestPoolSize = s;
}
}
} finally {
mainLock.unlock(); // 释放锁
}
if (workerAdded) {
container.start(t); // 启动线程
workerStarted = true;
}
}
} finally {
if (!workerStarted) {
// 如果线程启动失败,则回滚
addWorkerFailed(w);
}
}
return workerStarted;
}
看似也很多,实际并不复杂。前面两个循环就可以理解为业务代码里的数据校验:
- 检查线程池状态是否合法,是否可以新建线程
- 用于尝试增加线程计数,并处理 CAS 操作可能失败的情况。
就是为了在多线程的情况下能够处理线程池状态变化和cas计数器的变化,从而保证线程池行为正常;下面异常捕获里才是主逻辑:
- 创建worker
- w = new Worker(firstTask);:创建一个新的 Worker 对象。
- final Thread t = w.thread;:获取 Worker 对象中的线程。
- mainLock.lock();:锁定 mainLock。主要是对worker集合的操作线程安全
- 再次检查状态
- int c = ctl.get();:重新读取 ctl 的值。
- if (isRunning(c) || ...):检查线程池是否正在运行,或者状态小于 STOP 且没有初始任务。
- if (t.getState() != Thread.State.NEW):检查线程状态是否为 NEW。
- workers.add(w);:将 Worker 添加到集合中。
- workerAdded = true;:标记已添加 Worker。
- int s = workers.size();:获取当前 Worker 集合的大小。
- largestPoolSize = s;:更新最大线程数
- 解锁并启动线程
- mainLock.unlock();:释放锁。
- container.start(t);:启动线程。
- workerStarted = true;:标记线程已启动。
- 会滚和返回
- if (!workerStarted):如果线程启动失败,则回滚。
- addWorkerFailed(w);:处理失败情况。
- return workerStarted;:返回线程是否成功启动的结果。
2.3 runWorker
final void runWorker(Worker w) {
Thread wt = Thread.currentThread(); // 获取当前线程
Runnable task = w.firstTask; // 获取 Worker 对象的第一个任务
w.firstTask = null; // 清空 Worker 对象的第一个任务
w.unlock(); // 解锁,允许中断
boolean completedAbruptly = true; // 标记线程是否突然退出,默认为 true
try {
while (task != null || (task = getTask()) != null) { // 循环处理任务
w.lock(); // 加锁
// 如果线程池正在停止,确保线程被中断;否则,确保线程不被中断。
// 这里需要在第二种情况下进行重新检查,以处理在清除中断标志时的 `shutdownNow` 竞态条件
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted()) {
wt.interrupt(); // 中断当前线程
}
try {
beforeExecute(wt, task); // 执行任务前的钩子方法
try {
task.run(); // 执行任务
afterExecute(task, null); // 执行任务后的钩子方法
} catch (Throwable ex) {
afterExecute(task, ex); // 处理任务执行中的异常
throw ex; // 重新抛出异常
}
} finally {
task = null; // 清空任务
w.completedTasks++; // 增加已完成的任务数
w.unlock(); // 解锁
}
}
completedAbruptly = false; // 标记线程正常退出
} finally {
processWorkerExit(w, completedAbruptly); // 处理 Worker 线程退出
}
}
这里执行的方法就很容易读了
- 初始化变量
- Thread wt = Thread.currentThread(); // 获取当前线程
- Runnable task = w.firstTask; // 获取 Worker 对象的第一个任务
- w.firstTask = null; // 清空 Worker 对象的第一个任务
- w.unlock(); // 解锁,允许中断
- 状态校验:虽然写的一大堆 但总结一句话就是状态是stop或者之上就再次给你中断一下。
- 执行逻辑:这里有两个抽象方法afterExecute和beforeExecute供给你做一些自定义的逻辑。finally需要清空任务增加任务数同时解锁。
- processWorkerExit()方法 可以理解为工人生死簿
- 如果completedAbruptly=true就是线程意外退出则减少计数
- 更新完成的任务,完成了就把worker在集合中移除,判处死刑
- 所有任务都完成了 会尝试中止线程池
- 也会根据线程池的状态考虑是否需要增加worker或移除
2.4总结
到这里线程池核心干活的worker这部分就完事了,总结一下:
在线程池一个worker对应一个任务一个线程,worker的创建依赖于线程状态,任务的执行也是在worker里去处理。这里有个小技巧 worker类有一个firstTask在每个任务执行后会置空这个字段,同时下个任务来又继续如此,小细节避免了数据竞争也是线程安全的。
失败处理
线程池拒绝策略是实现的RejectedExecutionHandler接口内置有4个
- CallerRunsPolicy:调用者执行策略,当线程池线程数满时,它不再丢给线程池执行,也不丢弃掉,而是自己线程来执行,把异步任务变成同步任务。
- AbortPolicy:默认拒绝策略,直接抛出异常
- DiscardPolicy:丢弃策略会在提交任务失败时默默地把任务丢弃掉,失败就失败,完全不管它。其底层实现源码就是啥也不干
- DiscardOldestPolicy:丢弃最老任务策略,它就是目前等待最长时间也是最老的任务。
/**
* Invokes the rejected execution handler for the given command.
* Package-protected for use by ScheduledThreadPoolExecutor.
*/
final void reject(Runnable command) {
// 拒绝策略是在构造方法时传入的,默认为 RejectedExecutionHandler
// 即用户只需实现 rejectedExecution 方法,即可以自定义拒绝策略了
handler.rejectedExecution(command, this);
}
AbortPolicy 中止策略,线程池默认的拒绝策略,也是我们最常用的拒绝策略。当系统线程池满载的时候,可以通过异常的形式告知使用方,交由使用方自行处理。一般出现此异常时,我们可以提示用户稍后再试,或者我们把未执行的任务记录下来,等到适当时机再次执行。
shutdown
在前文,我们提到线程池通过 ctl 一共可以表示五种状态:
- RUNNING:运行状态。线程池接受并处理新任务。
- SHUTDOWN :关闭状态。线程池不能接受新任务,处理完剩余任务后关闭。调用
shutdown
方法会进入该状态。 - STOP:停止状态。线程池不能接受新任务,并且尝试中断旧任务。调用
shutdownNow
方法会进入该状态。 - TIDYING:整理状态。由关闭状态转变,线程池任务队列为空且没有任何工作线程时时进入该状态,会调用
terminated
方法。 - TERMINATED:终止状态。
terminated
方法执行完毕后进入该状态,线程池彻底停止。
它们具体的流转关系可以参考下图:
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();
}
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方法中对stop状态做进一步的区别。
2.3 真正的停机
我们已经知道通过 shutdownNow
与 shutdown
可以触发停机流程,当两个方法执行完毕后,线程池将会进入 STOP
或者 SHUTDOWN
状态。
但是此时线程池并未真正的停机,还记得runWorker方法中捕获后面有个finally块调用的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
阻塞的外部线程。
至此,线程池就真正的停机了。
再聊状态值
通过上面的过程,可以看到,整个ThreadPoolExecutor 非状态的依赖是非常强的。所以一个好的状态值的设计就显得很重要了。
。。。。。
线程池与线程状态
RUNNING:
线程池接受新任务,并处理阻塞队列中的任务。
工作线程正常运行,执行任务。
SHUTDOWN:
线程池不再接受新任务,但是仍然处理阻塞队列中的任务。
工作线程继续执行队列中的任务,直到队列为空。
STOP:
线程池不再接受新任务,并且尝试取消正在执行的任务,同时不处理阻塞队列中的任务。
工作线程会被中断,试图尽快退出。
TIDYING_UP:
所有的任务都已经完成,所有工作线程都在调用 workerCompleted() 方法。
线程池正在清理阶段,准备进入 TERMINATED 状态。
TERMINATED:
线程池已完成所有任务,所有工作线程都已经终止。
线程池处于最终状态,不能再接收新任务。
线程池中的线程本身也会处于操作系统的线程状态中的一种,比如:
运行状态(Running):线程正在执行任务。
阻塞状态(Blocked):线程正在等待某个锁或其他资源。
等待状态(Waiting):线程在等待某种条件成立,如等待任务队列中的任务。
就绪状态(Ready):线程已经准备好执行,等待CPU调度。
休眠状态(Timed Waiting):线程在等待一段时间后被唤醒,如 Thread.sleep()。
通过合理地管理线程的状态,线程池能够有效地利用系统资源,提高并发任务的执行效率
未完待续。。。。
回首
回答一下上面的思考:
-
线程池对线程的管理,不予回答
-
线程池减少开销如何实现:
减少线程创建开销:
创建一个新的线程涉及到分配内存、初始化线程上下文以及操作系统内核中的线程调度器对新线程的注册等一系列操作,这些操作都需要一定的计算资源。
使用线程池可以在程序启动时预先创建一定数量的线程,并让这些线程处于等待任务的状态。当有新的任务到来时,可以直接使用这些预先创建好的线程来执行任务,而无需再进行线程创建的过程。减少线程销毁开销:
当一个线程执行完任务后,如果没有线程池,这个线程就会被销毁。销毁线程同样涉及到释放内存、清理线程上下文以及操作系统内核中的注销操作。
线程池中的线程在执行完一个任务后并不会立即销毁,而是等待新的任务到来。这避免了频繁的线程销毁操作,节省了资源。线程复用:
线程池中的线程可以被多次复用来执行不同的任务,减少了线程创建和销毁的频率,从而提高了程序的性能。
未完待续。。。。。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器