1. 程式人生 > 其它 >面試突擊35:如何判斷執行緒池已經執行完所有任務了?

面試突擊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 種判斷執行緒池任務是否執行完的方法:

  1. 使用 isTerminated 方法判斷:通過判斷執行緒池的完成狀態來實現,需要關閉執行緒池,一般情況下不建議使用。
  2. 使用 getCompletedTaskCount 方法判斷:通過計劃執行總任務量和已經完成總任務量,來判斷執行緒池的任務是否已經全部執行,如果相等則判定為全部執行完成。但因為執行緒個體和狀態都會發生改變,所以得到的是一個大致的值,可能不準確。
  3. 使用 CountDownLatch 判斷:相當於一個執行緒安全的單次計數器,使用比較簡單,且不需要關閉執行緒池,是比較常用的判斷方法
  4. 使用 CyclicBarrier 判斷:相當於一個執行緒安全的重複計數器,但使用較為複雜,所以日常專案中使用的較少。

是非審之於己,譭譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

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