一文教你安全的關閉執行緒池

  • 2019 年 10 月 13 日
  • 筆記

上篇文章 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