面試官:說一說如何優雅的關閉執行緒池,我:shutdownNow,面試官:粗魯!

JavaBuild發表於2024-06-02

寫在開頭


面試官:“小夥子,執行緒池使用過嗎,來聊一聊它吧!”

我:“好的,然後巴拉巴拉一頓輸出之前看過的build哥執行緒池十八問...”

面試官滿意的點了點頭,緊接著問道:“那你知道如何優雅的關閉執行緒池嗎?”

我:“知道知道,直接呼叫shutdownNow()方法就好了呀!”

面試官臉色一變,微怒道:“粗魯!你給我滾出去!!!”


優雅的關閉執行緒池

哈哈,上面的場景是build哥臆想出來的面試畫面,我們現在步入正題,來看一看線上程池使用完成後如何優雅的關閉執行緒池。

在JDK 1.8 中,Java 併發工具包中 java.util.concurrent.ExecutorService 提供了 shutdown()、shutdownNow()這兩種介面方法去關閉執行緒池,我們分別看一下。

shutdown()

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主鎖
    mainLock.lock(); // 加鎖以確保獨佔訪問

    try {
        checkShutdownAccess(); // 檢查是否有關閉的許可權
        advanceRunState(SHUTDOWN); // 將執行器的狀態更新為SHUTDOWN
        interruptIdleWorkers(); // 中斷所有閒置的工作執行緒
        onShutdown(); // ScheduledThreadPoolExecutor中的掛鉤方法,可供子類重寫以進行額外操作
    } finally {
        mainLock.unlock(); // 無論try塊如何退出都要釋放鎖
    }
    tryTerminate(); // 如果條件允許,嘗試終止執行器
}

在shutdown的原始碼中,會啟動一次順序關閉,在這次關閉中,執行器不再接受新任務,但會繼續處理佇列中的已存在任務,當所有任務都完成後,執行緒池中的執行緒會逐漸退出。

我們寫一個小的demo來使用shutdown():

public class TestService{
    public static void main(String[] args) {
        //建立固定 3 個執行緒的執行緒池,測試使用,工作中推薦ThreadPoolExecutor
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        //向執行緒池提交 10 個任務
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            threadPool.submit(() -> {
                System.out.println("正在執行任務 " + index);
                //休眠 3 秒,模擬任務執行
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        //休眠 4 秒
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //關閉執行緒池
        threadPool.shutdown();
    }
}

在這段測試程式碼中,我們構造了一個包含固定3執行緒數的執行緒池,迴圈提交10個任務,每個任務休眠3秒,但主程式休眠4秒後,會掉用shutdown方法,理論上,在第二個時間迴圈中,執行緒池被停止,所以最多執行完6個任務,但從輸出中,我們絲毫感受不好執行緒何時被停止了。
輸出:

正在執行任務 1
正在執行任務 3
正在執行任務 2
正在執行任務 4
正在執行任務 5
正在執行任務 6
正在執行任務 7
正在執行任務 8
正在執行任務 9
正在執行任務 10

shutdownNow()

/**
 * 嘗試停止所有正在執行的任務,停止處理等待的任務,
 * 並返回等待處理的任務列表。
 *
 * @return 從未開始執行的任務列表
 */
public List<Runnable> shutdownNow() {
    List<Runnable> tasks; // 用於儲存未執行的任務的列表
    final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主鎖
    mainLock.lock(); // 加鎖以確保獨佔訪問
    try {
        checkShutdownAccess(); // 檢查是否有關閉的許可權
        advanceRunState(STOP); // 將執行器的狀態更新為STOP
        interruptWorkers(); // 中斷所有工作執行緒
        tasks = drainQueue(); // 清空佇列並將結果放入任務列表中
    } finally {
        mainLock.unlock(); // 無論try塊如何退出都要釋放鎖
    }
    tryTerminate(); // 如果條件允許,嘗試終止執行器
    
    return tasks; // 返回佇列中未被執行的任務列表
}

與shutdown不同的是shutdownNow會嘗試終止所有的正在執行的任務,清空佇列,停止失敗會丟擲異常,並且返回未被執行的任務列表。

由於shutdownNow會有返回值,所以我們將上面的測試案例稍作改動後輸出結果為:

image

這種會在控制檯丟擲異常的方式,同樣也不優雅,所以我們繼續往下看!

shutdown()+awaitTermination(long timeout, TimeUnit unit)

awaitTermination(long timeout, TimeUnit unit)是可以允許我們在呼叫shutdown方法後,再設定一個等待時間,如設定為5秒,則表示shutdown後5秒內執行緒池徹底終止,返回true,否則返回false;

這種方式裡,我們將shutdown()結合awaitTermination(long timeout, TimeUnit unit)方法去使用,注意在呼叫 awaitTermination() 方法時,應該設定合理的超時時間,以避免程式長時間阻塞而導致效能問題,而且由於這個方法在超時後也會丟擲異常,因此,我們在使用的時候要捕獲並處理異常!

public class TestService{
    public static void main(String[] args) {
        //建立固定 3 個執行緒的執行緒池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        //向執行緒池提交 10 個任務
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            threadPool.submit(() -> {
                System.out.println("正在執行任務 " + index);
                //休眠 3 秒
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        //關閉執行緒池,設定等待超時時間 3 秒
        System.out.println("設定執行緒池關閉,等待 3 秒...");
        threadPool.shutdown();
        try {
            boolean isTermination = threadPool.awaitTermination(3, TimeUnit.SECONDS);
            System.out.println(isTermination ? "執行緒池已停止" : "執行緒池未停止");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //再等待超時時間 20 秒
        System.out.println("再等待 20 秒...");
        try {
            boolean isTermination = threadPool.awaitTermination(20, TimeUnit.SECONDS);
            System.out.println(isTermination ? "執行緒池已停止" : "執行緒池仍未停止,請檢查!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出:

設定執行緒池關閉,等待 3 秒...
正在執行任務 1
正在執行任務 2
正在執行任務 3
正在執行任務 4
正在執行任務 5
執行緒池未停止
再等待 20 秒...
正在執行任務 6
正在執行任務 7
正在執行任務 8
正在執行任務 9
正在執行任務 10
執行緒池已停止

從輸出中我們可以看到,透過將兩種方法結合使用,我們監控了整個執行緒池關閉的全流程,實現了優雅的關閉!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
image

相關文章