写点什么

如何优雅的关闭 Java 线程池

作者:Geek_541d14
  • 2021 年 12 月 23 日
  • 本文字数:3854 字

    阅读完需:约 13 分钟

线程池提供了两个关闭方法,shutdownNowshutdown 方法。

shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。

shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

以上的说法虽然没错,但是还有很多的细节,比如调用shutdown方法后,正在执行任务的线程做出什么反应?正在等待任务的线程又做出什么反应?线程在什么情况下才会彻底退出。如果不了解这些细节,在关闭线程池时就难免遇到,像线程池关闭不了,关闭线程池出现报错等情况。

再说这些关闭线程池细节之前,需要强调一点的是,调用完 shutdownNowshuwdown 方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination方法进行同步等待。

以下源码取自 jdk1.8。

shutdownNow

public List<Runnable> shutdownNow() {  List<Runnable> tasks;  final ReentrantLock mainLock = this.mainLock;  mainLock.lock();  try {    checkShutdownAccess();    advanceRunState(STOP);  //1    interruptWorkers();  //2    tasks = drainQueue();  //3  } finally {    mainLock.unlock();  }  tryTerminate();  return tasks;}
复制代码

shutdownNow方法里,重要的三句代码我用红色数字标出来了。

第一句就是原子性的修改线程池的状态为 STOP 状态

第三句是将队列里还没有执行的任务放到列表里,返回给调用方。

第二句是遍历线程池里的所有工作线程,然后调用线程的 interrupt 方法。如下:

private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
复制代码

以上就是shutdownNow方法的执行逻辑:将线程池状态修改为 STOP,然后调用线程池里的所有线程的 interrupt 方法。

调用shutdownNow后,线程池里的线程会做如何反应呢?那就要看,线程池里线程正在执行的代码逻辑了。其在线程池的 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 pool is stopping, ensure thread is interrupted;                // if not, ensure thread is not interrupted.  This                // requires a recheck in second case to deal with                // shutdownNow race while clearing interrupt                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 循环里不停地执行。其中代码 task.run()就是在执行我们提交给线程池的任务,如当我们调用 shutdownNow 时,task.run()里面正处于 IO 阻塞,则会导致报错,如果 task.run()里正在正常执行,则不受影响,继续执行完这个任务。

从上面代码可以看出,如果 getTask()方法返回 null,也会导致线程的退出。我们再来看看 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; } }}
复制代码

如果我们调用shutdownNow方法时,线程处于从队列里读取任务而阻塞中,则会导致抛出 InterruptedException 异常,但因为异常被捕获,线程将会继续在这个 for 循环里执行。

还记得shutdownNow方法里第一步将线程修改为 STOP 状态吧,当执行到上边if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty()))判断时,由于 STOP 状态值是大于 SHUTDOWN 状态,STOP 也大于等于 STOP,不管任务队列是否为空,都会进入 if 语句从而返回 null,线程退出。

总结:当我们调用线程池的 shutdownNow 时,如果线程正在 getTask 方法中执行,则会通过 for 循环进入到 if 语句,于是 getTask 返回 null,从而线程退出。不管线程池里是否有未完成的任务。

如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错(如果任务里没有捕获 InterruptedException 异常),否则线程会执行完当前任务,然后通过 getTask 方法返回为 null 来退出

shutdown

再来看下shutdown方法的源码。

public void shutdown() {  final ReentrantLock mainLock = this.mainLock;  mainLock.lock();  try {    checkShutdownAccess();    advanceRunState(SHUTDOWN);    interruptIdleWorkers();    onShutdown(); // hook for ScheduledThreadPoolExecutor  } finally {    mainLock.unlock();  }  tryTerminate();}
复制代码

shutdownNow类似,只不过它是将线程池的状态修改为 SHUTDOWN 状态,然后调用 interruptIdleWorkers 方法,来中断空闲的线程。这是 interruptIdleWorkers 方法的实现:

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()) { // 注意这里有个w.tryLock()的判断        try {          t.interrupt();        } catch (SecurityException ignore) {        } finally {          w.unlock();        }      }      if (onlyOne)        break;    }  } finally {    mainLock.unlock();  }}
复制代码

shutdownNow方法调用 interruptWorkers 方法不同的是,interruptIdleWorkers 方法在遍历线程池里的线程时,有一个 w.tryLock()加锁判断,只有加锁成功的线程才会被调用 interrupt 方法。那什么情况下才能被加锁成功?什么情况下不能被加锁成功呢?这就需要我们继续回到线程执行的 runWorker 方法。

在上边 runWorker 方法代码中,请注意 w.lock()w.unlock()调用。其实就是正运行在 w.lock 和 w.unlock 之间的线程将因为加锁失败,而不会被调用 interrupt 方法,换句话说,就是正在执行线程池里任务的线程不会被中断。

不管是被调用了 interrupt 的线程还是没被调用的线程,什么时候退出呢?,这就要看 getTask 方法的返回是否为 null 了。

在 getTask 里的 if 判断(上文中 getTask 代码截图中上边红色方框的代码)中,由于线程池被 shutdown 方法修改为 SHUTDOWN 状态,SHUTDOWN 大于等于 SHUTDOWN 成立没问题,但是 SHUTDOWN 不再大于等于 STOP 状态,所以只有队列为空,getTask 方法才会返回 null,导致线程退出。

总结:当我们调用线程池的 shuwdown 方法时,如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。

如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。

总结

当我们调用 shutdownNow 或者 shutdown 方法去尝试关闭 java 线程池,都只是异步的通知线程池,此时线程池不会立即停止。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination

方法进行同步等待。

发布于: 2021 年 12 月 23 日
用户头像

Geek_541d14

关注

还未添加个人签名 2021.01.27 加入

还未添加个人简介

评论

发布
暂无评论
如何优雅的关闭 Java 线程池