很多場景下,我們需要等待線程池的所有任務都執行完,然后再進行下一步操作。對於線程 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面試真題解析