系列文章目录
文章目录
前言
上一篇文章讲了线程池的创建和启动,这一篇讲线程池的关闭。
可能刚开始大家会觉得线程池的关闭很容易理解,不就是调用shutdown()或shutdownNow()方法,中断线程,不再接受任务,等任务执行完,工作线程数量为0时,线程池就关闭了吗。但是,仔细想想,还是会有些疑惑:
1)中断线程时是怎么区分空闲线程和非空闲线程的呢?
2)中断空闲线程是怎么中断的?
3)中断非空闲线程是怎么中断的?如果一个线程正在执行任务,此时中断线程是真的将任务停止了吗?
带着疑惑,我们来深入理解线程池的关闭
一、线程池的5种状态
我们先回顾一下线程池的5种状态
1)RUNNING:能够接收新任务,以及对已添加的任务进行处理
2)SHUTDOWN:执行shutdown()方法会变成SHUTDOWN状态,不接收新任务,但能处理已添加的任务
3)STOP:不接收新任务,不处理已添加的任务,并且会中断正在处理的任务,执行shutdownNow()方法会变为STOP状态
4)TIDYING:所有的任务已终止,ctl记录的线程数量为0。会执行钩子函数terminated()
5)TERMINATED:执行完terminated()之后,就会由 TIDYING变为TERMINATED
二、shutdown()和shutdownNow()源码分析
1.shutdown()方法
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
主要做了这几件事:
1)advanceRunState(SHUTDOWN):自旋+CAS将线程池的状态修改为SHUTDOWN;
2)interruptIdleWorkers():中断空闲线程
3)onShutdown():钩子函数,留给子类实现
4)tryTerminate():向TERMINATED状态转变
所以我们要具体看interruptIdleWorkers()和tryTerminate()方法
interruptIdleWorkers()方法
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//遍历所有工作线程
for (Worker w : workers) {
Thread t = w.thread;
//如果没有被中断,并且在工作状态
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
遍历所有工作线程,如果没有被中断,并且在工作状态就对该线程中断,所以我们看一下w.tryLock()方法
public boolean tryLock() { return tryAcquire(1); }
上一篇文章讲过,worker实现了AQS的不可重入锁,这里实际上就是获取不可重入锁,获取到了就表示当前线程空闲,获取不到就表示当前线程正在工作
这里为啥用不可重入锁来表示空闲和工作状态,我的理解,首先肯定需要一个state变量来表示空闲和工作状态,所以这里就用了CAS的state,然后,为了避免在执行任务的过程中调用了shutdown方法而不会中断自己(因为自己本身正在执行任务),比如下面代码,如果是可重入锁,那么执行shutdown方法时,自己就把自己中断了(个人理解,可能不对,欢迎指正)
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i < 4; i++) {
Runnable task = () -> {
try {
// 模拟任务执行
Thread.sleep(2000);
//模拟任务中调用了shutdown方法
executorService.shutdown();
Thread.sleep(2000);
} catch (Exception e) {
// 处理中断异常
System.out.println(Thread.currentThread().getName()+"任务被中断。");
return;
}
System.out.println(Thread.currentThread().getName()+"任务结束。");
};
executorService.execute(task);
}
}
ps: 其实没必要像我一样深究这些,越底层的框架越需要严谨,所以实现会变得复杂,过于深究太浪费时间,我们只需要知道这里使用不可重入锁来表示空闲和工作状态即可
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) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
1)自旋,如果是以下两种情况,可以去关闭线程池,其他情况(比如正在运行,比如工作线程数不为0,比如已经关闭)直接返回
a.线程池状态为SHUTDOWN并且工作线程数为0,等待队列为空
b.线程池状态为STOP并且工作线程数为0
2)关闭线程池:
cas设置线程池状态为TIDYING成功后(因为前面有自旋,所以到这里了,要么成功,要么到第一步判断线程池已经关闭了就直接返回),执行terminated()方法(子类实现),最后将线程池状态设为TERMINATED
写到这里,其实shutdown方法主要就是对空闲线程进行了中断(执行thread.interrupt方法),那到底中断后会怎么样呢?
中断空闲线程
首先我们回顾一下runWorker方法,代码如下:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
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);
}
}
上一篇讲过,runWorker方法就是循环获取任务,执行任务。前面又说过,线程的空闲和工作状态通过worker实现的不可重入锁来区分,runWorker方法一旦获取到任务,就会调用w.lock()方法,将线程变为工作状态。所以要中断的空闲线程一定是调用w.lock()方法之前,主要是正在获取任务时(获取到任务后,调用w.lock()方法之前这种特殊情况后面再分析)。所以我们要分析在调用getTask方法时线程被中断会发生什么?
先看下getTask方法的代码:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
这里分两种情况
1)第一种情况:代码执行到if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) 这里时,线程池状态已经为SHUTDOWN了,这时,如果等待队列为空,线程就直接被干掉了
2)第二种情况:代码执行到Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :workQueue.take();如果任务队列为空,线程便阻塞。这时,shutdown方法将线程池状态变为SHUTDOWN,并且执行了中断方法。那么线程中断会让workQueue.poll或者workQueue.take在阻塞中唤醒(之前讲condition队列时讲过,如果中断发生在signal前则抛异常,如果中断发生在signal之后不抛异常,但终归都是被唤醒了),唤醒之后如果获取不到任务,又会循环进入第一种情况
综上,shutdown方法中断空闲线程,就是在任务队列为空时,让获取不到任务的空闲线程立刻被干掉(那些被阻塞的线程会被立刻唤醒),这样印证了前面介绍的SHUTDOWN状态下不接收新任务,但能处理已添加的任务
2.shutdownNow()
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
跟shutdown方法差不多:
1)advanceRunState(STOP):自旋+CAS将线程池的状态修改为STOP;
2)interruptWorkers():中断所有线程
3)tasks = drainQueue():将队列里还没有执行的任务放到列表里,返回给调用方
4)tryTerminate():向TERMINATED状态转变
这里我们只分析中断所有线程的过程,先说中断空闲线程:
中断空闲线程
上面分析过了shutdown方法中断空闲线程,shutdownNow方法中断空闲线程的逻辑也类似
1)如果在判断if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty()))之前线程池状态变为了STOP,那么不论任务队列中是否有任务,都会被停止。这也印证了前面说的STOP状态下不接收新任务,不处理已添加的任务。
2)如果在之后,除非线程已经获取到了任务,否则也会被停止
很容易理解,就不展开了
再说中断正在工作的线程:
中断正在工作的线程
还是要从runWorker方法说起,上面粘贴过代码,这里只粘贴一部分
while (task != null || (task = getTask()) != null) {
//将线程状态修改为工作状态
w.lock();
// 这里的判断是为了保证在状态为SHUTDOWN时,即使被中断,也能继续执行任务
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();
}
}
前面说过,正在工作的线程就是执行完w.lock()的线程,那么会有2种情况
1)shutdownNow方法的中断工作线程发生在执行完w.lock()后,调用task.run()之前
2)shutdownNow方法的中断工作线程发生在调用task.run()之后
我们知道,如果任务要在被中断后结束线程,那么必须在任务代码中手动判断中断信号,比如判断有中断信号后抛错,或者调用能响应中断的方法,比如Thread.sleep(),lock.lockInterruptly在中断后会抛错,所以,针对上面两种情况
1)第一种情况,如果task.run()能响应中断,那么会被中断,如果不能响应,则不能中断
2)第二种情况,不论task.run()能不能响应中断,都不会被中断
我还是上一个示例代码吧
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable task = () -> {
// 模拟任务执行
try {
Thread.sleep(2000);
// for (int j = 0; j < 10000000; j++) {
// if (j == 9999999)
// System.out.println(Thread.currentThread().getName() + ":" + j);
// }
} catch (Exception e) {
// 处理中断异常
System.out.println("任务被中断。");
return;
}
System.out.println("任务正在执行。");
};
executorService.execute(task);
}
// 关闭ExecutorService,并试图停止所有正在执行和暂停的任务
executorService.shutdownNow();
// 等待所有任务都停止
while (!executorService.isTerminated()) {
}
System.out.println("服务已关闭。");
}
执行结果
任务被中断。
任务被中断。
任务被中断。
任务被中断。
任务被中断。
服务已关闭。
如果把任务改成如下:
// 模拟任务执行
try {
// Thread.sleep(2000);
for (int j = 0; j < 10000000; j++) {
if (j == 9999999)
System.out.println(Thread.currentThread().getName() + ":" + j);
}
执行结果为:
pool-1-thread-1:9999999
任务正在执行。
pool-1-thread-4:9999999
任务正在执行。
pool-1-thread-2:9999999
任务正在执行。
pool-1-thread-5:9999999
任务正在执行。
pool-1-thread-3:9999999
任务正在执行。
服务已关闭。
显然,因为for循环中的代码并不能响应中断,所以会继续执行完
再把代码调整一下,改为用shutdown方法,任务还是调用thread.sleep:
try {
Thread.sleep(2000);
}
//中间省略...
executorService.shutdown();
执行结果为:
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
任务正在执行。
服务已关闭。
可以看到,shutdown方法会把任务队列中的任务继续执行完
3.runWorker方法的深究
到这里,其实这篇文章已经可以结束了,但是我就怕有些人喜欢跟我一样深究,因为runWorker方法中有一段代码困扰了我很久,如下
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
先说结论,我的理解是:这里的判断是为了保证在状态为SHUTDOWN时,即使被中断,也能继续执行完任务
咋一看,有点难以理解,我们慢慢分析:
因为前面在分析shutdown方法的时候说过,有一种极端情况,获取到任务后,调用w.lock()方法之前也属于空闲线程,那么这时候也会被中断,但是前面说过线程池的SHUTDOWN状态是需要处理任务队列中的任务的,既然任务队列中的任务都能处理,那么已经获取到任务后更应该能处理啊,此时线程就不应该被中断了啊,所以这段代码中Thread.interrupted() 就将中断标记清除了,同时也不影响STOP状态下的中断,因为STOP状态下,又通过 wt.interrupt()重新标记上了。这样,就保证了在状态为SHUTDOWN时,即使被中断,也能继续执行任务(ps:可能有杠精说,为啥要保证在状态为SHUTDOWN时,即使被中断,也能继续执行完任务,中断了这个任务就不执行了呗,^_^,人家写这个框架的李二狗就是这样划分线程池状态的,SHUTDOWN状态下就是要能执行任务)
这一节只针对喜欢深究的人,不深究的就跳过
总结
- worker实现了AQS的不可重入锁来表示空闲和工作状态
- shutdown方法中断空闲线程,就是在任务队列为空时,让获取不到任务的空闲线程立刻被停止
- shutdownNow方法中断线程时,对于空闲线程,除非线程已经获取到了任务,否则不论任务队列是否有任务,都会被停止;
- shutdownNow方法中断线程时,对于正在工作的线程,如果task.run()能响应中断,且中断发生在响应中断的代码之前,那么会被停止,否则不能停止;如果task.run()不能响应中断,则线程不能停止