关于线程池:如何判断线程池已经执行完所有任务了

100次阅读

共计 6613 个字符,预计需要花费 17 分钟才能阅读完成。

很多场景下,咱们须要期待线程池的所有工作都执行完,而后再进行下一步操作。对于线程 Thread 来说,很好实现,加一个 join 办法就解决了,然而对于线程池的判断就比拟麻烦了。

咱们本文提供 4 种判断线程池工作是否执行完的办法:

应用 isTerminated 办法判断。
应用 getCompletedTaskCount 办法判断。
应用 CountDownLatch 判断。
应用 CyclicBarrier 判断。
接下来咱们一个一个来看。

不判断的问题
如果不对线程池是否曾经执行完做判断,就会呈现以下问题,如下代码所示:

import java.util.Random;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolCompleted {public static void main(String[] args) {
        // 创立线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
                0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
        // 增加工作
        addTask(threadPool);
                // 打印后果
        System.out.println("线程池工作执行实现!");
    }
  
    /**
     * 给线程池增加工作
     */
    private static void addTask(ThreadPoolExecutor threadPool) {
        // 工作总数
        final int taskCount = 5;
        // 增加工作
        for (int i = 0; i < taskCount; i++) {
            final int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 随机休眠 0-4s
                        int sleepTime = new Random().nextInt(5);
                        TimeUnit.SECONDS.sleep(sleepTime);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(String.format("工作 %d 执行实现", finalI));
                }
            });
        }
    }
}
复制代码

以上程序的执行后果如下:

从上述执行后果能够看出,程序先打印了“线程池工作执行实现!”,而后还在陆续的执行线程池的工作,这种执行程序凌乱的后果,并不是咱们冀望的后果。咱们想要的后果是等所有工作都执行完之后,再打印“线程池工作执行实现!”的信息。

产生以上问题的起因是因为主线程 main,和线程池是并发执行的,所以当线程池还没执行完,main 线程的打印后果代码就曾经执行了。想要解决这个问题,就须要在打印后果之前,先判断线程池的工作是否曾经全副执行完,如果没有执行完就期待工作执行完再执行打印后果。

办法 1:isTerminated
咱们能够利用线程池的终止状态(TERMINATED)来判断线程池的工作是否曾经全副执行完,但想要线程池的状态产生扭转,咱们就须要调用线程池的 shutdown 办法,不然线程池始终会处于 RUNNING 运行状态,那就没方法应用终止状态来判断工作是否曾经全副执行完了,它的实现代码如下:

import java.util.Random;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 线程池工作执行实现判断
 */
public class ThreadPoolCompleted {public static void main(String[] args) {
        // 1. 创立线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
                0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
        // 2. 增加工作
        addTask(threadPool);
        // 3. 判断线程池是否执行完
        isCompleted(threadPool); //【外围调用办法】// 4. 线程池执行完
        System.out.println();
        System.out.println("线程池工作执行实现!");
    }

    /**
     * 办法 1:isTerminated 实现形式
     * 判断线程池的所有工作是否执行完
     */
    private static void isCompleted(ThreadPoolExecutor threadPool) {threadPool.shutdown();
        while (!threadPool.isTerminated()) {// 如果没有执行完就始终循环}
    }

    /**
     * 给线程池增加工作
     */
    private static void addTask(ThreadPoolExecutor threadPool) {
        // 工作总数
        final int taskCount = 5;
        // 增加工作
        for (int i = 0; i < taskCount; i++) {
            final int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 随机休眠 0-4s
                        int sleepTime = new Random().nextInt(5);
                        TimeUnit.SECONDS.sleep(sleepTime);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    System.out.println(String.format("工作 %d 执行实现", finalI));
                }
            });
        }
    }
}
复制代码

办法阐明:shutdown 办法是启动线程池有序敞开的办法,它在齐全敞开之前会执行完之前所有曾经提交的工作,并且不会再承受任何新工作。当线程池中的所有工作都执行完之后,线程池就进入了终止状态,调用 isTerminated 办法返回的后果就是 true 了。

以上程序的执行后果如下:

毛病剖析
须要敞开线程池。

扩大:线程池的所有状态
线程池总共蕴含以下 5 种状态:

RUNNING:运行状态。
SHUTDOWN:敞开状态。
STOP:阻断状态。
TIDYING:整顿状态。
TERMINATED:终止状态。

如果不调用线程池的敞开办法,那么线程池会始终处于 RUNNING 运行状态。

办法 2:getCompletedTaskCount
咱们能够通过判断线程池中的打算执行工作数和已实现工作数,来判断线程池是否曾经全副执行完,如果打算执行工作数 = 已实现工作数,那么线程池的工作就全副执行完了,否则就未执行完,具体实现代码如下:

/**
 * 办法 2:getCompletedTaskCount 实现形式
 * 判断线程池的所有工作是否执行完
 */
private static void isCompletedByTaskCount(ThreadPoolExecutor threadPool) {while (threadPool.getTaskCount() != threadPool.getCompletedTaskCount()) {}}
复制代码

以上程序执行后果如下:

办法阐明
getTaskCount():返回打算执行的工作总数。因为工作和线程的状态可能在计算过程中动态变化,因而返回的值只是一个近似值。
getCompletedTaskCount():返回实现执行工作的总数。因为工作和线程的状态可能在计算过程中动静地扭转,所以返回的值只是一个近似值,然而在间断的调用中并不会缩小。
优缺点剖析
此实现办法的长处是无需敞开线程池。它的毛病是 getTaskCount() 和 getCompletedTaskCount() 返回的是一个近似值,因为线程池中的工作和线程的状态可能在计算过程中动态变化,所以它们两个返回的都是一个近似值。

办法 3:CountDownLatch
CountDownLatch 能够了解为一个计数器,咱们创立了一个蕴含 N 个工作的计数器,每个工作执行完计数器 -1,直到计数器减为 0 时,阐明所有的工作都执行完了,就能够执行下一段业务的代码了,它的实现流程具体实现代码如下:

public static void main(String[] args) throws InterruptedException {
    // 创立线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
        0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
    final int taskCount = 5;    // 工作总数
    // 单次计数器
    CountDownLatch countDownLatch = new CountDownLatch(taskCount); // ①
    // 增加工作
    for (int i = 0; i < taskCount; i++) {
        final int finalI = i;
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 随机休眠 0-4s
                    int sleepTime = new Random().nextInt(5);
                    TimeUnit.SECONDS.sleep(sleepTime);
                } catch (InterruptedException e) {e.printStackTrace();
                }
                System.out.println(String.format("工作 %d 执行实现", finalI));
                // 线程执行完,计数器 -1
                countDownLatch.countDown();  // ②}
        });
    }
    // 阻塞期待线程池工作执行完
    countDownLatch.await();  // ③
    // 线程池执行完
    System.out.println();
    System.out.println("线程池工作执行实现!");
}
复制代码

代码阐明:以上代码中标识为 ①、②、③ 的代码行是外围实现代码,其中:① 是申明一个蕴含了 5 个工作的计数器;② 是每个工作执行完之后计数器 -1;③ 是阻塞期待计数器 CountDownLatch 减为 0,示意工作都执行完了,能够执行 await 办法前面的业务代码了。

以上程序的执行后果如下:

优缺点剖析
CountDownLatch 写法很优雅,且无需敞开线程池,但它的毛病是只能应用一次,CountDownLatch 创立之后不能被重复使用,也就是说 CountDownLatch 能够了解为只能应用一次的计数器。

办法 4:CyclicBarrier
CyclicBarrier 和 CountDownLatch 相似,它能够了解为一个能够重复使用的循环计数器,CyclicBarrier 能够调用 reset 办法将本人重置到初始状态,CyclicBarrier 具体实现代码如下:

public static void main(String[] args) throws InterruptedException {
    // 创立线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 20,
        0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1024));
    final int taskCount = 5;    // 工作总数
    // 循环计数器 ①
    CyclicBarrier cyclicBarrier = new CyclicBarrier(taskCount, new Runnable() {
        @Override
        public void run() {
            // 线程池执行完
            System.out.println();
            System.out.println("线程池所有工作已执行完!");
        }
    });
    // 增加工作
    for (int i = 0; i < taskCount; i++) {
        final int finalI = i;
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 随机休眠 0-4s
                    int sleepTime = new Random().nextInt(5);
                    TimeUnit.SECONDS.sleep(sleepTime);
                    System.out.println(String.format("工作 %d 执行实现", finalI));
                    // 线程执行完
                    cyclicBarrier.await(); // ②} catch (InterruptedException e) {e.printStackTrace();
                } catch (BrokenBarrierException e) {e.printStackTrace();
                }
            }
        });
    }
}
复制代码

以上程序的执行后果如下:

办法阐明
CyclicBarrier 有 3 个重要的办法:

构造方法:构造方法能够传递两个参数,参数 1 是计数器的数量 parties,参数 2 是计数器为 0 时,也就是工作都执行完之后能够执行的事件(办法)。
await 办法:在 CyclicBarrier 上进行阻塞期待,当调用此办法时 CyclicBarrier 的外部计数器会 -1,直到产生以下情景之一:
在 CyclicBarrier 上期待的线程数量达到 parties,也就是计数器的申明数量时,则所有线程被开释,继续执行。
以后线程被中断,则抛出 InterruptedException 异样,并进行期待,继续执行。
其余期待的线程被中断,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
其余期待的线程超时,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
其余线程调用 CyclicBarrier.reset() 办法,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
reset 办法:使得 CyclicBarrier 回归初始状态,直观来看它做了两件事:
如果有正在期待的线程,则会抛出 BrokenBarrierException 异样,且这些线程进行期待,继续执行。
将是否破损标记位 broken 置为 false。
优缺点剖析
CyclicBarrier 从设计的复杂度到应用的复杂度都高于 CountDownLatch,相比于 CountDownLatch 来说它的长处是能够重复使用(只需调用 reset 就能复原到初始状态),毛病是应用难度较高。

总结
咱们本文提供 4 种判断线程池工作是否执行完的办法:

应用 isTerminated 办法判断:通过判断线程池的实现状态来实现,须要敞开线程池,个别状况下不倡议应用。
应用 getCompletedTaskCount 办法判断:通过打算执行总任务量和曾经实现总任务量,来判断线程池的工作是否曾经全副执行,如果相等则断定为全副执行实现。但因为线程个体和状态都会产生扭转,所以失去的是一个大抵的值,可能不精确。
应用 CountDownLatch 判断:相当于一个线程平安的单次计数器,应用比较简单,且不须要敞开线程池,是比拟罕用的判断办法。
应用 CyclicBarrier 判断:相当于一个线程平安的反复计数器,但应用较为简单,所以日常我的项目中应用的较少。
最初
如果你感觉此文对你有一丁点帮忙,点个赞。或者能够退出我的开发交换群:1025263163 互相学习,咱们会有业余的技术答疑解惑

如果你感觉这篇文章对你有点用的话,麻烦请给咱们的开源我的项目点点 star:http://github.crmeb.net/u/defu 不胜感激!

残缺源码下载地址:https://market.cloud.tencent….

PHP 学习手册:https://doc.crmeb.com
技术交换论坛:https://q.crmeb.com

正文完
 0