一、前言
在我们日常的开发过程中,我们经常会开启多个线程或者创建一个线程池去执行多个并发任务,当所有任务执行完毕后,我们一般会做一个统一的处理。那我们如何知道多个线程的任务已经全部执行完毕了呢?
今天由我来为大家介绍几种方法:
- isTerminated() 判断方式,在执行 shutdown() ,关闭线程池后,判断是否所有任务已经完成。
- ThreadPoolExecutor 的 getCompletedTaskCount() 方法,判断完成任务数和全部任务数是否相等。
- CountDownLatch 计数器,使用闭锁计数来判断是否全部完成。
- 手动维护一个公共计数 ,原理和闭锁类似,就是更加灵活。
- 使用 submit 向线程池提交任务,Future 判断任务执行状态。
二、isTerminated
这里有两个主要方法:
- shutdown() :启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务。如果已经关闭,调用没有额外的作用。
- isTerminated() :如果所有任务在关闭后完成,则返回true。请注意, isTerminated 从不是 true,除非 shutdown 或 shutdownNow 先被执行。
通俗点讲,就是在执行全部任务后,对线程池进行 shutdown() 有序关闭,然后循环判断 isTerminated() ,线程池是否全部完成。另外,最新最全的 Java 面试题整理好了,点击Java面试库小程序在线刷题。
- 优点 :操作简单,代码更加简单。
- 缺点 :需要关闭线程池。一般我在代码中都是将线程池注入到 Spring 容器,然后各个组件中统一用同一个,当然不能关闭。
类似方法扩展:
- shutdownNow() :尝试停止所有主动执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。从此方法返回时,这些任务将从任务队列中删除。通过 Thread.interrupt() 取消任务。
- isShutdown() :如果线程池已关闭,则返回 true 。
- isTerminating() :如果在 shutdown() 或 shutdownNow() 之后终止 ,但尚未完全终止,则返回true。
- waitTermination(long timeout, TimeUnit unit) :当前线程阻塞,直到等所有已提交的任务(包括正在跑的和队列中等待的)执行完,或者等超时时间到,或者线程被中断抛出异常;全部执行完返回true,超时返回false。也可以用这个方法代替 isTerminated() 进行判断 。
三、getCompletedTaskCount
还是一样在主线程循环判断,主要就两个方法:
- getTaskCount() :返回计划执行的任务总数。由于任务和线程的状态可能在计算过程中动态变化,因此返回的值只是一个近似值。
- getCompletedTaskCount() :返回完成执行的任务的大致总数。因为任务和线程的状态可能在计算过程中动态地改变,所以返回的值只是一个近似值,但是在连续的调用中并不会减少。
这个好理解,总任务数等于已完成任务数,就表示全部执行完毕。
- 优点 :完全使用了 ThreadPoolExecutor 提供的方法,并且不必关闭线程池,避免了创建和销毁带来的损耗。
- 缺点 :上面的解释也看到了,使用这种判断存在很大的限制条件;必须确定,在循环判断过程中,没有新的任务产生。差不多意思就是,这个线程池只能在这条线程中使用。
四、CountDownLatch和CyclicBarrier
CountDownLatch 和 CyclicBarrier 类似,都是等待所有任务到达某个点之后,再进行后续的操作
CountDownLatch 使用的示例代码如下:
java复制代码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 创建之后不能被重复使用。
CyclicBarrier 和 CountDownLatch 类似,它可以理解为一个可以重复使用的循环计数器,CyclicBarrier 可以调用 reset 方法将自己重置到初始状态,CyclicBarrier 具体实现代码如下:
java复制代码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 就能恢复到初始状态),缺点是使用难度较高。
五、公共计数
这种方式其实和(CountDownLatch和CyclicBarrier)原理类似。
先维护一个静态变量
private static int taskNum = 0;
然后在线程任务结束时,进行静态变量操作:
private static void staticCountTest() throws Exception {
Lock lock = new ReentrantLock();
for (int i = 0; i < 30; i++) {
int index = i;
pool.execute(() -> {
sleepMtehod(index);
lock.lock();
taskNum++;
lock.unlock();
});
}
while(taskNum < 30) {
Thread.sleep(1000);
System.out.println("还没停止。。。当前完成任务数:" + taskNum);
}
System.out.println("全部执行完毕");
}
其实就是加锁计数,循环判断。
-
优点 :手动维护方式更加灵活,对于一些特殊场景可以手动处理。
-
缺点 :和
CountDownLatch
相比,一样需要知道线程数目,但是代码实现比较麻烦,相对于灵活这一个优势,貌似投入产出并不对等。
六、Future
Future
是用来装载线程结果的,不过,用这个来进行判断写代码总感觉怪怪的。
因为 Future
只能装载一条线程的返回结果,多条线程总不能用 List 在接收 Future 。
这里就开一个线程做个演示:
private static void futureTest() throws Exception {
Future<?> future = pool.submit(() -> sleepMtehod(1));
while (!future.isDone()){
Thread.sleep(500);
System.out.println("还没停止。。。");
}
System.out.println("全部执行完毕");
}
这种方式就不写优缺点了,因为 Future
的主要使用场景并不是用于判断任务执行状态。