编辑
2023-12-05
学习记录
00
请注意,本文编写于 484 天前,最后修改于 484 天前,其中某些信息可能已经过时。

目录

前提
方案
代码实现
统计完成已完成任务数
使用 FutureTask
使用CountDownLatch
使用CyclicBarrier
使用isTerminated()
总结

前提

在项目中有很多情况下需要使用多线程的方式执行任务,那么有什么比较好的方法来判断多线程任务执行完毕,我们再进行下一步的操作,之前的话我会设置等待时间来等待

java
executor.awaitTermination(30, TimeUnit.MINUTES);

方案

  • 使用 getCompletedTaskCount() 统计出【已完成任务数】和使用Java线程池中的getTaskCount() 方法来获取【总任务数】,二者进行对比即可。
  • 使用 FutureTask对象 ,等待所有任务都执行完,线程池的任务就都执行完了。
  • 使用 CountDownLatch对象 或 CyclicBarrier对象,等待所有线程都执行完之后,再执行后续流程,计数。
  • 使用isTerminated() 方法。利用线程池的终止状态(TERMINATED)来判断线程池的任务是否已经全部执行完,但想要线程池的状态发生改变,就需要调用线程池的 shutdown() 方法,不然线程池一直会处于 RUNNING 运行状态,那就没办法使用终止状态来判断任务是否已经全部执行完了,shutdown() 方法是启动线程池有序关闭的方法,它在完全关闭之前会执行完之前所有已经提交的任务,并且不会再接受任何新任务。当线程池中的所有任务都执行完之后,线程池就进入了终止状态,调用 isTerminated() 方法返回的结果就是 true 了,以这点作为依据来判断即可。

代码实现

统计完成已完成任务数

java
package com.example.demo.component.threadPool; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CountThreadTask { //创建一个最大线程数100的线程池 private static ExecutorService es = new ThreadPoolExecutor(3, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100)); public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { int finalI = i; es.execute(() -> { //提交执行 System.out.println("线程" + finalI + "执行完成!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }); } ThreadPoolExecutor threadPool = ((ThreadPoolExecutor) es); System.out.println("线程池任务总数量:"+threadPool.getTaskCount()); System.out.println("---------线程池开始执行-----------"); while (true) { if (threadPool.getTaskCount() == threadPool.getCompletedTaskCount()) { System.out.println("---------线程池执行完了-----------"); break; } //间隔2s查询一次 Thread.sleep(2000); System.out.println("线程池还未执行完,敬请等待!已完成的任务数量:"+threadPool.getCompletedTaskCount()); } } }
  • getTaskCount():返回线程池计划执行的任务总数。注意:由于任务和线程的状态可能在计算过程中动态变化,因此该方法返回值只是一个近似值,不是精准的。

  • getCompletedTaskCount():返回线程池中已完成的任务数,注意:跟getTaskCount()方法一致,该方法返回值也是一个近似值。

  • getPoolSize():返回线程池当前的线程数量。

  • getActiveCount():返回当前线程池中正在执行任务的线程数量。

方式总结:

由于getTaskCount() 与 getCompletedTaskCount()方法返回值都是一个近似值而不是精确值,固结果可能有一定的偏差,这也是该方式的一大缺点。

使用 FutureTask

java
package com.example.demo.component.threadPool; import java.util.concurrent.*; /** * 使用 FutrueTask 等待线程池执行完全部任务 */ public class FutureTaskTask { //创建一个最大线程数100的线程池 private static ExecutorService es = new ThreadPoolExecutor(4, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100)); public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建任务1 FutureTask<Integer> task1 = new FutureTask<>(() -> { System.out.println("---Task 1 开始执行---"); Thread.sleep(2000); System.out.println("------Task 1 执行结束------"); return 1; }); // 创建任务2 FutureTask<Integer> task2 = new FutureTask<>(() -> { System.out.println("---Task 2 开始执行---"); Thread.sleep(3000); System.out.println("------Task 2 执行结束------"); return 2; }); // 创建任务3 FutureTask<Integer> task3 = new FutureTask<>(() -> { System.out.println("---Task 3 开始执行---"); Thread.sleep(1000); System.out.println("------Task 3 执行结束------"); return 3; }); // 创建任务4 FutureTask<Integer> task4 = new FutureTask<>(() -> { System.out.println("---Task 4 开始执行---"); Thread.sleep(500); System.out.println("------Task 4 执行结束------"); return 4; }); // 提交4个任务给线程池 es.submit(task1); es.submit(task2); es.submit(task3); es.submit(task4); // 等待所有任务执行完毕 task1.get(); task2.get(); task3.get(); task4.get(); //执行完毕 System.out.println("线程池执行完了!"); } }

使用CountDownLatch

CountDownLatch身为同步工具类,作用之一可协调多个线程之间的同步,或者说接通线程之间的通信(而不是互斥)。CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后再继续执行。其中,计数器初始值为全线程的数量,当每一个线程完成自己任务后,计数器的值就会自动减1;当计数器的值 = 0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

java
package com.example.demo.component.threadPool; import java.util.concurrent.*; /** * 使用CountDownLatch */ public class CountDownLatchTask { //创建一个最大线程数100的线程池 private static ExecutorService es = new ThreadPoolExecutor(1, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100)); public static void main(String[] args) throws Exception { //计数器,判断线程是否执行结束 //初始值为10 CountDownLatch taskLatch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { es.execute(() -> { //提交执行 try { //模拟线程执行方法,执行1s Thread.sleep(1000); taskLatch.countDown(); System.out.println("当前计数器值为:" + taskLatch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } }); } //当前线程阻塞,等待计数器置为0 taskLatch.await(); System.out.println("线程池执行完了!"); } }

方式总结:

虽然使用CountDownLatch可达到统计线程是否被执行完,该方式使用起来代码简洁优雅,不需要对线程池进行操作。但由于CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

使用CyclicBarrier

 CyclicBarrier 和 CountDownLatch 类似,你可以把它理解为一个可以重复使用的循环计数器,CyclicBarrier 可调用 reset() 方法将自己重置到初始状态,这是与CountDownLatch不一样的特性,那具体如何使用CyclicBarrier达到统计线程池所有线程都被执行完的需求吧

java
package com.example.demo.component.threadPool; import java.util.Random; import java.util.concurrent.*; /** * 使用CyclicBarrier */ public class CyclicBarrierTask { //创建一个最大线程数100的线程池 private static ExecutorService es = new ThreadPoolExecutor(5, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100)); public static void main(String[] args) throws InterruptedException { //任务总数 final int taskCount = 5; //循环计数器 CyclicBarrier cyclicBarrier = new CyclicBarrier(taskCount, new Runnable() { @Override public void run() { // 线程池执行完 System.out.println("---------线程池执行完了-----------"); } }); // 添加任务 for (int i = 0; i < taskCount; i++) { final int finalI = i; es.submit(new Runnable() { @Override public void run() { try { //随机休眠1-4秒 TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println("任务" + finalI + "执行完成"); // 线程执行完 cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }); } } }

使用isTerminated()

使用线程池的 isTerminated() 方法,在执行 shutdown() 进行线程池的关闭后, 隔间调用isTerminated()判断线程池中的所有任务是否已经完成即可。那具体如何使用 isTerminated() 方法达到统计线程池所有线程都被执行完的需求吧

java
package com.example.demo.component.threadPool; import java.util.concurrent.*; /** * 使用isTerminated() */ public class IsTerminatedTask { //创建一个最大线程数100的线程池 private static ExecutorService es = new ThreadPoolExecutor(4, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100)); public static void main(String[] args) throws Exception { for (int i = 1; i <= 10; i++) { int finalI = i; es.execute(() -> { //提交执行 System.out.println("线程" + finalI + "执行完成!"); try { //模拟线程执行过程 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } //关闭线程池 es.shutdown(); //隔间1s判断是否执行完了,如果所有任务在关闭后完成,返回true。 while (!es.isTerminated()) { Thread.sleep(1000); } System.out.println("---------线程池执行完了-----------"); } }

总结

  1. 使用getCompletedTaskCount()和getTaskCount() 方法 优点:使用它可不需要进行线程池的关闭,避免了创建线程池及销毁所带来的内存开销。 缺点:使用它两方法返回的都是一个近似值,而且进行线程判断局限很大,要保证在循环判断过程中没有产生新的任务,否则该方式就统计失效了。
  2. 使用 FutureTask 优点:使用其方法就是主打一个精确值,使用简单优雅,不需要对线程池有任何的操作。 缺点:每个提交给线程池的任务都会关联一个FutureTask对象,这就可能会损耗额外的内存开销。如果需要处理大量的任务,可能会占用较大的内存资源。
  3. 使用CountDownLatch  优点:使用简单优雅,不需要对线程池有任何的操作。 缺点:使用CountDownLatch 计数器只能使用一次,CountDownLatch 创建之后不能重复使用,而且需要提前知道线程的数量,性能较差,还需要在线程代码块内加上异常判断,否则在 countDown()之前发生异常而没有处理,就会导致主线程永远阻塞在 await 。
  4. 使用CyclicBarrier  优点:使用简单优雅,计数器可重置进行重复使用。 缺点:使用难度较高。相比CountDownLatch而言,CyclicBarrier 无论从设计还是使用,复杂度都高于CountDownLatch,相比 CountDownLatch 而言它的优点就是可以重复使用。
  5. 使用isTerminated() 优点:使用简单优雅。 缺点:使用场景受限,需要shutdown()关闭线程池。因为日常使用是会将线程池注入到Spring容器里,然后各个组件中都统一用同一个线程池,不能直接关闭线程池。

本文作者:Weee

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!