乐趣区

关于java:面试突击35如何判断线程池已经执行完所有任务了

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

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

  1. 应用 isTerminated 办法判断。
  2. 应用 getCompletedTaskCount 办法判断。
  3. 应用 CountDownLatch 判断。
  4. 应用 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. 构造方法:构造方法能够传递两个参数,参数 1 是计数器的数量 parties,参数 2 是计数器为 0 时,也就是工作都执行完之后能够执行的事件(办法)。
  2. await 办法:在 CyclicBarrier 上进行阻塞期待,当调用此办法时 CyclicBarrier 的外部计数器会 -1,直到产生以下情景之一:

    1. 在 CyclicBarrier 上期待的线程数量达到 parties,也就是计数器的申明数量时,则所有线程被开释,继续执行。
    2. 以后线程被中断,则抛出 InterruptedException 异样,并进行期待,继续执行。
    3. 其余期待的线程被中断,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
    4. 其余期待的线程超时,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
    5. 其余线程调用 CyclicBarrier.reset() 办法,则以后线程抛出 BrokenBarrierException 异样,并进行期待,继续执行。
  3. reset 办法:使得 CyclicBarrier 回归初始状态,直观来看它做了两件事:

    1. 如果有正在期待的线程,则会抛出 BrokenBarrierException 异样,且这些线程进行期待,继续执行。
    2. 将是否破损标记位 broken 置为 false。

      优缺点剖析

      CyclicBarrier 从设计的复杂度到应用的复杂度都高于 CountDownLatch,相比于 CountDownLatch 来说它的长处是能够重复使用(只需调用 reset 就能复原到初始状态),毛病是应用难度较高。

      总结

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

  4. 应用 isTerminated 办法判断:通过判断线程池的实现状态来实现,须要敞开线程池,个别状况下不倡议应用。
  5. 应用 getCompletedTaskCount 办法判断:通过打算执行总任务量和曾经实现总任务量,来判断线程池的工作是否曾经全副执行,如果相等则断定为全副执行实现。但因为线程个体和状态都会产生扭转,所以失去的是一个大抵的值,可能不精确。
  6. 应用 CountDownLatch 判断:相当于一个线程平安的单次计数器,应用比较简单,且不须要敞开线程池,是 比拟罕用的判断办法
  7. 应用 CyclicBarrier 判断:相当于一个线程平安的反复计数器,但应用较为简单,所以日常我的项目中应用的较少。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java 面试真题解析

面试合集:https://gitee.com/mydb/interview

退出移动版