如何正确的停止线程池

3,663 阅读3分钟

这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战

如何停止线程池,有五个方法是和它相关的,并通过代码去把演示。ThreadPoolExecutor 中涉及到关闭线程池的方法有以下五种:

void shutdown();
boolean isShutdown();
boolean isTerminated();
boolean awaitTerminated(long timeout,TimeUnit unit) throws InterruptedException;
List<Runnable> shutdownNow();

下面我们对这些方法逐一分析。

shutdown()

  第一种最简单直白的方法叫 shutdown(),此方法可以安全地关闭一个线程池,但是大家不要轻易的被表面所迷惑,实际上调用 shutdown 方法之后,线程池并不是立刻就被关闭。事实上这个方法仅仅是初始化整个关闭过程,因为这个时候线程池中可能还有很多任务正在被执行,或者是任务队列中有大量正在等待被执行的任务,所以不是调用 shutdown 方法就立即关闭。在执行这个方法之后,线程池就接收到关闭信息,所以这个时候线程池为了优雅起见,会把正在执行的任务以及队列中等待的任务都执行完毕之后再关闭。

但是并不代表 shutdow() 操作是没有任何效果的,调用 shutdown() 方法方法之后呢,对于线程池而言,知道想让关闭,如果再有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务,或抛出拒绝异常。所以并不是执行了 shutdown() 方法之后什么都没做,一旦执行了shutdown() 方法之后,虽然会把已有的任务全都执行完毕,但是新的任务就不会增加了,所以 shutdown 方法就是这样一个特点。

下面就来用代码来演示一下。具体代码如下:

public class ShutDownPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new TaskShutDownPool());
        }
       Thread.sleep(1000);
        
        executorService.shutdown();
        Thread.sleep(500);
        executorService.execute(new TaskShutDownPool());
    }
}
class TaskShutDownPool implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

首先,我们创建一个线程数固定为 10 的线程池,那这个时候创建的线程的种类就不是很关键了,并且往线程中提交 1000 个任务。同时在调用 shutdown() 方法之后,再一次提交我们的任务。

image.png

通过执行我们的程序,我们知道执行 shutdown() 之后,实际上它并不会停止,而是会把存量的任务都执行完毕。再提交新的任务是提交不进去的,会抛出 RejectedExecutionException 异常,意思是我们提交的任务已经被拒绝了,这个就是 shutdown() 方法的作用。

但是这个时候,这种方法并不满足我们的需求,通过使用 shutdown() 方法是不是只能通过新提交任务才能判断出是不是已经停止了,那有没有其他办法能直接看出线程有没有停。确实是有办法的,可以通过 isShutdown() 方法。

isShutdown

isShutdown() 方法可以返回一个布尔值,true 或者 false 来判断线程池是不是已经开始关闭工作,也就是是否执行了 shutdown() 或者 shutdownNow() 方法。这个停止不是说完全停止,因为完全停止指的是所有的任务都执行完毕。

isShutdown() 方法所判断的是是不是进入停止状态了,也就是一旦执行了 shutdwn() 方法,isShutdown() 方法就会返回 true,并不代表线程池此时已经彻底关闭了,仅仅代表线程池开始了关闭的流程,也就是,此时可能线程池中依然有在执行任务,队列里也肯能有等待被执行的任务。

我们通过代码来验证一下,这里还是使用前面的代码.

System.out.println("结果:"+executorService.isShutdown()+",调用 shutdown() 方法之前");
Thread.sleep(500);
executorService.shutdown();
System.out.println("结果:"+executorService.isShutdown()+",调用 shutdown() 方法之后");

执行我们的程序,结果如下所示:

image.png

首先打印出还没有执行 shutdown() 方法之前,线程池还处于正在运行状态,返回的结果是 false,当执行 shutdown() 方法之后,线程池处于停止状态,返回的结果是 false。并且可以看出,即便线程现在还在运,但是已经调用 isShutdown() 方法,并且在控制台输出 true。

所以这个时候大家要记清楚,不是返回 true,这个线程池就完全结束了,而是只是开始结束就已经返回 true。

此时,大家还是对这种需求不满意了,我们还想知道这个线程池是不是真的停止了,而不是表面现象,那怎么办呢?此时还有一个方法来满足我们的需求,此时要使用到 isTerminated() 方法。

isTerminated()

isTerminated() 这个方法它是可以返回我们整个线程是不是已经完全终止了,这不仅仅线程池已经关闭,同时代表线程池中的所有任务都执行完毕了,就是线程池里面的线程包括正在执行的任务以及队列里面的任务都执行完了。因为前面我们说过,调用 shutdown() 方法之后,线程池会继续执行里面未完成的任务,不仅包括线程正在执行的任务,还包括正在队列中等待的任务。

下面我们通过代码来验证一下,具体代码如下所示:

Thread.sleep(1000);
System.out.println("结果:"+executorService.isTerminated()+",调用 shutdown() 方法之前");
Thread.sleep(500);
executorService.shutdown();


System.out.println("结果:"+executorService.isTerminated()+",调用 shutdown() 方法之后");

执行结果如下所示:

image.png

在调用 shutdown() 方法之后,但是有一些线程依然在执行任务,调用 isShutdown() 方法返回的是 true,而调用 itTerminated() 方法返回的是 false,因为这个时候还有很多的线程还没有执行完,线程池并没有真正的停止。所有的任务执行完毕了,调用 isTerminated() 方法才会返回 true,这个表示线程已关闭并且线程池内部是空,所有的任务都执行完毕了。

awaitTermination()

第四个方法是 awaitTermination()。这个方法是什么作用呢?首先,这个方法呀作用相对比较弱,它不是用来停止线程池的,而是主要用来判断线程池的状态的。比如我们给 awaitTermination() 方法传入的参数是 10 秒,那么它就会等待 10 秒钟。

调用 awaitTermination() 方法之后,当前线程会只是等待一段时间,如果在等待的这段时间内,线程池已经关闭并且内部任务都执行完毕了,这个方法会返回true,否则超时会返回 false。所以这个方法只是一个用来测试在一段时间内这个线程是不是完全停止的。它起到的主要作用是检测,而不是关闭。运行这个方法也不代表就执行了 shutdown。

下面我们就通过代码演示该方法的实际运行的情况。具体代码如下所示:

executorService.shutdown();
System.out.println(executorService.awaitTermination(10L, TimeUnit.SECONDS)+",执行了 shutdown() 方法");

这个 10 秒钟期间,这个线程是会阻塞在这里的,就是说他一直是停在这个方法中的,直到 10 秒钟之后再往下执行,而且它会返回一个布尔值,然后我们把这个布尔值给打出来。

image.png

即便提前把它给停止掉,但是检测的是 10 秒钟之内是不是完全运行完毕了,所以控制台输出结果还是 false。是否执行 shutdown 和 awaitTermination 是没有任何关系的。

假设我们等待的时间长一点,控制台就会输出 true,假设我们设置的等待时间是 60 秒,让所有的任务都运行完毕,那么返回的结果就是 true。

image.png

通过运行程序,在控制台输出 true,因为在这个期间所有的任务都执行完毕了,那么就满足 awaitTermination() 方法等待的时间,最后返回 true。

接下来我们来总结一下 awaitTermination() 方法返回条件。awaitTermination() 方法只有三种情况会返回,在返回之前它是阻塞的,直到发生以下这三种情况之一:

第一种情况等待期间(包括进入等待状态之前)线程池已关闭并且所有已提交的任务(包括正在执行的和队列中等待的)都执行完毕,相当于线程池已经“终结”了,方法便会返回 true。

第二种情况等待超时时间到后,第一种线程池“终结”的情况始终未发生,方法返回 false。

第三种情况等待期间线程被中断,方法会抛出 InterruptedException 异常。

所以最后就会根据是不是执行完毕,如果执行完毕就返回 true,如果没执行完毕,它就返回 false,代表已超时。所以 awaitTermination() 方法主要是用来做一些判断,判断下一步应该执行的操作。

shutdownNow()

还有最后一个方法是 shutdownNow(),这个方法比较暴力,它与前面我们介绍的方法都不一样,这个方法后面带了一个 Now,也就表示立刻关闭的意思。如果要想立刻关闭掉,我们作为线程池的设计者,我们想一下应该怎么办,比较优雅。在执行 shutdownNow() 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务List来进行一些补救的操作,例如记录在案并在后期重试。

比如我们有 10 个线程正在运行中,那么当我们的线程池执行 shutdownNow() 方法之后,这 10 个线程就会收到 interrupt 的信号,也就是说它们将会被中断了。我们还有一部分线程,就是正在队列中等待的那部分任务,假设我们的队列里面存着 100 个任务,还没有轮到他们执行,10 个线程正在执行的任务其实不是队列里面的,队列里面的任务还在等待,所以这部分的任务也需要我们处理,所以怎么办呢?这一部分任务会直接返回,返回是一个列表。

下面我们就通过代码来演示 shutdownNo() 方法的使用,具体代码如下:

public class ShutDownPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new TaskShutDownPool());
        }
        Thread.sleep(1000);
        List<Runnable> runnableList=executorService.shutdownNow();
for (Runnable runnableList1:runnableList) {
    System.out.println(runnableList1);
}
    }
}
class TaskShutDownPool implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+",被中断了");
        }
    }
}

运行结果如下所示:

image.png

通过执行的结果可以看出 shutdownNow() 方法的威力是比较大的,同时可以将队列中任务返回。

现在我们来看一下 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;
}

你可以看到源码中有一行 interruptWorkers() 代码,这行代码会让每一个已经启动的线程都中断,这样线程就可以在执行任务期间检测到中断信号并进行相应的处理,提前结束任务。这里需要注意的是,由于Java中不推荐强行停止线程的机制的限制,即便我们调用了 shutdownNow 方法,如果被中断的线程对于中断信号不理不睬,那么依然有可能导致任务不会停止。可见我们在开发中落地最佳实践是很重要的,我们自己编写的线程应当具有响应中断信号的能力,正确停止线程的方法在第⒉讲有讲过,应当利用中断信号来协同工作。

如何选择停止线程池方法

在掌握了这5种关闭线程池相关的方法之后,我们就可以根据自己的业务需要,选择合适的方法来停止线程池,比如通常我们可以用 shutdown() 方法来关闭,这样可以让已提交的任务都执行完毕,但是如果情况紧急,那我们就可以用 shutdownNow() 方法来加快线程池“终结”的速度。