一文教你安全的關閉線程池

上篇文章 ShutdownHook- Java 優雅停機解決方案 提到應用停機時需要釋放資源,關閉連接。對於一些定時任務或者網絡請求服務將會使用線程池,當應用停機時需要正確安全的關閉線程池,如果處理不當,可能造成數據丟失,業務請求結果不正確等問題。

關閉線程池我們可以選擇什麼都不做,JVM 關閉時自然的會清除線程池對象。當然這麼做,存在很大的弊端,線程池中正在執行執行的線程以及隊列中還未執行任務將會變得極不可控。所以我們需要想辦法控制到這些未執行的任務以及正在執行的線程。

線程池 API 提供兩個主動關閉的方法 ThreadPoolExecutor#shutdownNowThreadPoolExecutor#shutdown,這兩個方法都可以用於關閉線程池,但是具體效果卻不太一樣。

線程池的狀態

在說線程池關閉方法之前,我們先了解線程池狀態。

線程池狀態關係圖如下:

線程池狀態.png

從上圖我們看到線程池總共存在 5 種狀態,分別爲:

  • RUNNING:線程池創建之後的初始狀態,這種狀態下可以執行任務。
  • SHUTDOWN:該狀態下線程池不再接受新任務,但是會將工作隊列中的任務執行結束。
  • STOP: 該狀態下線程池不再接受新任務,但是不會處理工作隊列中的任務,並且將會中斷線程。
  • TIDYING:該狀態下所有任務都已終止,將會執行 terminated() 鉤子方法。
  • TERMINATED:執行完 terminated() 鉤子方法之後。

當我們執行 ThreadPoolExecutor#shutdown 方法將會使線程池狀態從 RUNNING 轉變爲 SHUTDOWN。而調用 ThreadPoolExecutor#shutdownNow 之後線程池狀態將會從 RUNNING 轉變爲 STOP。從上面的圖上還可以看到,當線程池處於 SHUTDOWN,我們還是可以繼續調用 ThreadPoolExecutor#shutdownNow 方法,將其狀態轉變爲 STOP

ThreadPoolExecutor#shutdown

上面我們知道線程池狀態,這裏先說說 shutdown 方法。shutdown 方法源碼比較簡單,能比較直觀理解其調用邏輯。

shutdown 方法源碼:

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
       // 檢查權限
            checkShutdownAccess();
            // 設置線程池狀態
        advanceRunState(SHUTDOWN);
       // 中斷空閒線程
            interruptIdleWorkers();
            // 鉤子函數,主要用於清理一些資源
            onShutdown(); 
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

shutdown 方法首先加鎖,其次先檢查系統安裝狀態。接着就會將線程池狀態變爲 SHUTDOWN,在這之後線程池不再接受提交的新任務。此時如果還繼續往線程池提交任務,將會使用線程池拒絕策略響應,默認情況下將會使用 ThreadPoolExecutor.AbortPolicy,拋出 RejectedExecutionException 異常。

interruptIdleWorkers 方法只會中斷空閒的線程,不會中斷正在執行任務的的線程。空閒的線程將會阻塞在線程池的阻塞隊列上。

線程池構造參數需要指定 coreSize(核心線程池數量),maximumPoolSize(最大的線程池數量),keepAliveTime(多餘空閒線程等待時間),unit(時間單位),workQueue(阻塞隊列)。

當調用線程池的 execute 方法,線程池工作流程如下:

  1. 如果此時線程池中線程數量小於 coreSize,將會新建線程執行提交的任務。
  2. 如果此時線程池線程數量已經大於 coreSize,將會直接把任務加入到隊列中。線程將會從工作隊列中獲取任務執行。
  3. 如果工作隊列已滿,將會繼續新建線程。
  4. 如果工作隊列已滿,且線程數等於 maximumPoolSize,此時將會使用拒絕策略拒絕任務。
  5. 超過 coreSize 數量那部分線程,如果空閒了 keepAliveTime ,線程將會終止。

工作流程圖如下:

線程池執行流程圖.png

當線程池處於第二步時,線程將會使用 workQueue#take 獲取隊頭的任務,然後完成任務。如果工作隊列一直沒任務,由於隊列爲阻塞隊列,workQueue#take 將會阻塞線程。

ThreadPoolExecutor#shutdownNow

ThreadPoolExecutor#shutdownNow 源碼如下:

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        // 檢查狀態
            checkShutdownAccess();
        // 將線程池狀態變爲 STOP
            advanceRunState(STOP);
            // 中斷所有線程,包括工作線程以及空閒線程
        interruptWorkers();
        // 丟棄工作隊列中存量任務
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

shutdownNow 方法將會把線程池狀態設置爲 STOP,然後中斷所有線程,最後取出工作隊列中所有未完成的任務返回給調用者。

對比 shutdown 方法,shutdownNow 方法比較粗暴,直接中斷工作線程。不過這裏需要注意,中斷線程並不代表線程立刻結束。這裏需要線程主動配合線程中斷響應。

線程中斷機制:
thread#interrupt 只是設置一箇中斷標誌,不會立即中斷正常的線程。如果想讓中斷立即生效,必須在線程 內調用 Thread.interrupted() 判斷線程的中斷狀態。
對於阻塞的線程,調用中斷時,線程將會立刻退出阻塞狀態並拋出 InterruptedException 異常。所以對於阻塞線程需要正確處理 InterruptedException 異常。

awaitTermination

線程池 shutdownshutdownNow 方法都不會主動等待執行任務的結束,如果需要等到線程池任務執行結束,需要調用 awaitTermination 主動等待任務調用結束。

調用方法如下:

        threadPool.shutdown();
        try {
            while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
                System.out.println("線程池任務還未執行結束");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

如果線程池任務執行結束,awaitTermination 方法將會返回 true,否則當等待時間超過指定時間後將會返回 false

如果需要使用這種進制,建議在上面的基礎上增加一定重試次數。這個真的很重要!!!

優雅關閉線程池

回顧上面線程池狀態關係圖,我們可以知道處於 SHUTDOWN 的狀態下的線程池依舊可以調用 shutdownNow。所以我們可以結合 shutdownshutdownNowawaitTermination ,更加優雅關閉線程池。

        threadPool.shutdown(); // Disable new tasks from being submitted
        // 設定最大重試次數
        try {
            // 等待 60 s
            if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
                // 調用 shutdownNow 取消正在執行的任務
                threadPool.shutdownNow();
                // 再次等待 60 s,如果還未結束,可以再次嘗試,或則直接放棄
                if (!threadPool.awaitTermination(60, TimeUnit.SECONDS))
                    System.err.println("線程池任務未正常執行結束");
            }
        } catch (InterruptedException ie) {
            // 重新調用 shutdownNow
            threadPool.shutdownNow();
        }

文章首發於 studyidea.cn/close..

歡迎關注我的公衆號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn

其他平臺.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章