如何优雅地关闭线程池?从源码剖析线程池的正确销毁姿势。
本篇约2千字,阅读时长约5分钟。你以为线程池的销毁只是调用shutdown()或者shutdownNow()吗?
一、前言
二、shutdown()将runState流转为SHUTDOWN
1、shutdown是如何只中断空闲线程的?
2、为什么要调用tryTerminate()?
3、termination.signalAll()唤醒的是什么?
三、shutdownNow()将runState流转为STOP
四、shutdown()和shutdownNow()的区别
五、关闭线程池的正确姿势
六、总结
一、前言
当关闭一个线程池时,有的工作线程还正在执行任务,有的调用者正在向线程池提交任务,并且工作队列中可能还有未执行的任务。因此,关闭过程不可能是瞬时的,而是一个平滑过渡的过程。
二、shutdown()将runState流转为SHUTDOWN
shutdown()
主要做了4步:
-
检查是否有中断线程池的权限。 -
自旋 CAS
设置runStatus
为SHUTDOWN
-
中断空闲线程。 -
尝试终止线程池。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//1.检查是否有Shutdown权限
checkShutdownAccess();
//2.cas设置runStatus为SHUTDOWN
advanceRunState(SHUTDOWN);
//3.中断空闲线程
interruptIdleWorkers();
//空的钩子函数
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
//4.尝试终止线程池
tryTerminate();
}
检查权限和设置状态都很简单,shutdown
是如何只中断空闲线程?为什么要调用tryTerminate()
?
1、shutdown是如何只中断空闲线程的?
从源码中可以看出在中断线程前会尝试获取Worker
的锁,如果获得锁,说明当前的Worker
是空闲的,可以中断,这也验证了Worker执行任务代码时加锁,确保除了线程池销毁导致中断外,没有其他中断的设置。
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
//循环中断空闲worder
Thread t = w.thread;
//w.tryLock()尝试获取worker的锁,如果获得锁说明当前工作线程是空闲的
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
2、为什么要调用tryTerminate()?
tryTerminate()
不会强行终止线程池,当workerCount
为0,workerQueue
为空时:
-
状态流转到 TIDYING
。 -
然后调用钩子函数 terminated()
。 -
状态从 TIDYING
流转到TERMINATED
。 -
调用 termination.sinaglAll()
,通知前面阻塞在awaitTermination
的所有调用者线程。
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;
}
//1.当workQueue为空,workerCount为空时,cas流转状态为TIDYING
//2.并调用了一个空钩子函数terminated
//3.最终将状态流转为TERMINATED,并通知awaitTermination
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 自旋
}
}
3、termination.signalAll()唤醒的是什么?
awaitTermination()
逻辑很简单,就是重复判断runState
是否到达最终状态TERMINATED,如果是直接返回true,如果不是,调用termination.awaitNanos(nanos)
阻塞一段时间,苏醒后再判断一次,如果runState
是TERMINATED
返回true,否则返回false。
/**
* Wait condition to support awaitTermination
*/
private final Condition termination = mainLock.newCondition();
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (;;) {
////判断当前的runstate是否大于等于TERMINATED
if (runStateAtLeast(ctl.get(), TERMINATED))
return true;
if (nanos <= 0)
return false;
//如果不是TERMINATED,将等待nanos
nanos = termination.awaitNanos(nanos);
}
} finally {
mainLock.unlock();
}
}
三、shutdownNow()将runState流转为STOP
shutdownNow()
和shutdown()
相似,主要做了5步:
-
检查是否有中断线程池权限。 -
设置 runStatus
为STOP
。 -
中断所有工作线程。 -
清空工作队列。 -
尝试终止线程池。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//1.检查是否有shutdown权限
checkShutdownAccess();
//2.设置runStatus为stop
advanceRunState(STOP);
//3.给worders发送终止信号
interruptWorkers();
//4.清空阻塞队列
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//5.尝试终止线程池
tryTerminate();
return tasks;
}
四、shutdown()和shutdownNow()的区别
shutdownNow()
和shutdown()
不同之处在于:
-
shutdownNow()
会终止所有工作线程,不管是空闲还是正在运行。 -
shutdownNow()
会清空阻塞队列。
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
private List<Runnable> drainQueue() {
BlockingQueue<Runnable> q = workQueue;
ArrayList<Runnable> taskList = new ArrayList<Runnable>();
//删除所有元素并加入到taskList
q.drainTo(taskList);
if (!q.isEmpty()) {
//如果q此时又被加入了任务再将任务删除并加入taskList
for (Runnable r : q.toArray(new Runnable[0])) {
if (q.remove(r))
taskList.add(r);
}
}
return taskList;
}
五、关闭线程池的正确姿势
分析源码之后可知只调用shutdown()
或者shutdownNow()
是不够的,因为线程池并不一定立刻终止,还需要调用awaitTermination
。
如下摘自tomcat中对线程池的销毁处理:
-
首先是业务方面的销毁,如销毁容器组件,释放资源等。 -
调用shutdownNow()立刻销毁线程池。 -
调用awaitTermination等待线程池销毁。 -
最后检查线程池销毁状态(isTerminating),未销毁则打印日志或者告警通知。
// 业务destory省略
// 此时需要立刻停止线程池
threadPoolExecutor.shutdownNow();
try {
threadPoolExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Ignore
}
if (threadPoolExecutor.isTerminating()) {
System.out.println("warn.executorShutdown, alarm to user");
}
if (threadPoolExecutor.isTerminated()) {
System.out.println("destory...");
}
shutdownNow()
可以换成shutdown()
,视业务情况而定,如果需要线程池中正在执行的任务or还在队列中的任务正常执行完,就调用shutdown()
,但是注意,如果有一个工作线程执行任务卡住一直不结束,就会导致线程池一直不销毁,所以最后的awaitTermination + check
是必要。
六、总结
-
shutdown()
会中断空闲工作线程,不会中断正在执行任务的工作线程,也不会清空工作队列,会等待所有已提交的任务执行完,但是拒绝新提交的任务。 -
shutdownNow()
,会中断所有工作线程,并清空工作队列,拒绝新提交的任务。 -
关闭线程池,只调用 shutdown()
或者shutdownNow()
是不够的,因为线程池并不一定立刻终止,还需要调用awaitTermination
并检查线程池是否销毁,没有销毁还需要提醒使用者。