很多场景下,咱们须要期待线程池的所有工作都执行完,而后再进行下一步操作。对于线程 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 判断:相当于一个线程平安的反复计数器,但应用较为简单,所以日常我的项目中应用的较少。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java 面试真题解析
面试合集:https://gitee.com/mydb/interview