[Java併發-23-併發設計模式] 兩階段終止模式:優雅地終止線程

前面咱們都是在講如何建立線程,接下來咱們說下如何終止線程java

java的線程小節中,我曾講過:線程執行完或者出現異常就會進入終止狀態。這樣看,終止一個線程看上去很簡單啊!一個線程執行完本身的任務,本身進入終止狀態,這的確很簡單。不過咱們今天談到的「優雅地終止線程」,不是本身終止本身,而是在一個線程 T1 中,終止線程 T2;這裏所謂的「優雅」,指的是給 T2 一個機會料理後事,而不是被直接終止。segmentfault

Java 語言的 Thread 類中曾經提供了一個 stop() 方法,用來終止線程,但是早已不建議使用了,緣由是這個方法用是直接終止的線程,線程並無機會料理後事。設計模式

如何理解兩階段終止模式

前輩們通過認真對比分析,已經總結出了一套成熟的方案,叫作兩階段終止模式。顧名思義,就是將終止過程分紅兩個階段,其中第一個階段主要是線程 T1 向線程 T2發送終止指令,而第二階段則是線程 T2響應終止指令併發

兩階段終止模式示意圖###性能

那在 Java 語言裏,終止指令是什麼呢?這個要從 Java 線程的狀態轉換過程提及。咱們在 java的線程小節中曾經提到過 Java 線程的狀態轉換圖。線程

從這個圖裏你會發現,Java 線程進入終止狀態的前提是線程進入 RUNNABLE 狀態,而實際上線程也可能處在休眠狀態,也就是說,咱們要想終止一個線程,首先要把線程的狀態從休眠狀態轉換到 RUNNABLE 狀態。如何作到呢?這個要靠 Java Thread 類提供的interrupt() 方法,它能夠將休眠狀態的線程轉換到 RUNNABLE 狀態。設計

線程轉換到 RUNNABLE 狀態以後,咱們如何再將其終止呢?RUNNABLE 狀態轉換到終止狀態,優雅的方式是讓 Java 線程本身執行完 run() 方法,因此通常咱們採用的方法是設置一個標誌位,而後線程會在合適的時機檢查這個標誌位,若是發現符合終止條件,則自動退出 run() 方法。這個過程其實就是咱們前面提到的第二階段:響應終止指令代理

綜合上面這兩點,咱們能總結出終止指令,其實包括兩方面內容:interrupt() 方法線程終止的標誌位code

用兩階段終止模式終止監控操做

實際工做中,有些監控系統須要動態地採集一些數據,通常都是監控系統發送採集指令給被監控系統的監控代理,監控代理接收到指令以後,從監控目標收集數據,而後回傳給監控系統,詳細過程以下圖所示。出於對性能的考慮(有些監控項對系統性能影響很大,因此不能一直持續監控),動態採集功能通常都會有終止操做。隊列

動態採集功能示意圖###

下面的示例代碼是監控代理簡化以後的實現,start() 方法會啓動一個新的線程 rptThread 來執行監控數據採集和回傳的功能,stop() 方法須要優雅地終止線程 rptThread,那 stop() 相關功能該如何實現呢?

class Proxy {
  boolean started = false;
  // 採集線程
  Thread rptThread;
  // 啓動採集功能
  synchronized void start(){
    // 不容許同時啓動多個採集線程
    if (started) {
      return;
    }
    started = true;
    rptThread = new Thread(()->{
      while (true) {
        // 省略採集、回傳實現
        report();
        // 每隔兩秒鐘採集、回傳一次數據
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {  
        }
      }
      // 執行到此處說明線程立刻終止
      started = false;
    });
    rptThread.start();
  }
  // 終止採集功能
  synchronized void stop(){
    // 如何實現?
  }
}

按照兩階段終止模式,咱們首先須要作的就是將線程 rptThread 狀態轉換到 RUNNABLE,作法很簡單,只須要在調用 rptThread.interrupt() 就能夠了。線程 rptThread 的狀態轉換到 RUNNABLE 以後,如何優雅地終止呢?下面的示例代碼中,咱們選擇的標誌位是線程的中斷狀態:Thread.currentThread().isInterrupted() ,須要注意的是,咱們在捕獲 Thread.sleep() 的中斷異常以後,經過 Thread.currentThread().interrupt() 從新設置了線程的中斷狀態,由於 JVM 的異常處理會清除線程的中斷狀態。

class Proxy {
  boolean started = false;
  // 採集線程
  Thread rptThread;
  // 啓動採集功能
  synchronized void start(){
    // 不容許同時啓動多個採集線程
    if (started) {
      return;
    }
    started = true;
    rptThread = new Thread(()->{
      while (!Thread.currentThread().isInterrupted()){
        // 省略採集、回傳實現
        report();
        // 每隔兩秒鐘採集、回傳一次數據
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          // 從新設置線程中斷狀態
          Thread.currentThread().interrupt();
        }
      }
      // 執行到此處說明線程立刻終止
      started = false;
    });
    rptThread.start();
  }
  // 終止採集功能
  synchronized void stop(){
    rptThread.interrupt();
  }
}

上面的示例代碼的確可以解決當前的問題,可是建議你在實際工做中謹慎使用。緣由在於咱們極可能在線程的 run() 方法中調用第三方類庫提供的方法,而咱們沒有辦法保證第三方類庫正確處理了線程的中斷異常,例如第三方類庫在捕獲到 Thread.sleep() 方法拋出的中斷異常後,沒有從新設置線程的中斷狀態,那麼就會致使線程不可以正常終止。因此強烈建議你設置本身的線程終止標誌位,例如在下面的代碼中,使用 isTerminated 做爲線程終止標誌位,此時不管是否正確處理了線程的中斷異常,都不會影響線程優雅地終止。

class Proxy {
  // 線程終止標誌位
  volatile boolean terminated = false;
  boolean started = false;
  // 採集線程
  Thread rptThread;
  // 啓動採集功能
  synchronized void start(){
    // 不容許同時啓動多個採集線程
    if (started) {
      return;
    }
    started = true;
    terminated = false;
    rptThread = new Thread(()->{
      while (!terminated){
        // 省略採集、回傳實現
        report();
        // 每隔兩秒鐘採集、回傳一次數據
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e){
          // 從新設置線程中斷狀態
          Thread.currentThread().interrupt();
        }
      }
      // 執行到此處說明線程立刻終止
      started = false;
    });
    rptThread.start();
  }
  // 終止採集功能
  synchronized void stop(){
    // 設置中斷標誌位
    terminated = true;
    // 中斷線程 rptThread
    rptThread.interrupt();
  }
}

如何優雅地終止線程池

線程池提供了兩個方法:shutdown()和shutdownNow()

咱們曾經講過,Java 線程池是生產者 - 消費者模式的一種實現,提交給線程池的任務,首先是進入一個阻塞隊列中,以後線程池中的線程從阻塞隊列中取出任務執行。

shutdown() 方法是一種很保守的關閉線程池的方法。線程池執行 shutdown() 後,就會拒絕接收新的任務,可是會等待線程池中正在執行的任務和已經進入阻塞隊列的任務都執行完以後才最終關閉線程池。

而 shutdownNow() 方法,相對就激進一些了,線程池執行 shutdownNow() 後,會拒絕接收新的任務,同時還會中斷線程池中正在執行的任務,已經進入阻塞隊列的任務也被剝奪了執行的機會,不過這些被剝奪執行機會的任務會做爲 shutdownNow() 方法的返回值返回。由於 shutdownNow() 方法會中斷正在執行的線程,因此提交到線程池的任務,若是須要優雅地結束,就須要正確地處理線程中斷。

總結

兩階段終止模式是一種應用很普遍的併發設計模式,在 Java 語言中使用兩階段終止模式來優雅地終止線程,須要注意兩個關鍵點:一個是僅檢查終止標誌位是不夠的,由於線程的狀態可能處於休眠態;另外一個是僅檢查線程的中斷狀態也是不夠的,由於咱們依賴的第三方類庫極可能沒有正確處理中斷異常。

當你使用 Java 的線程池來管理線程的時候,須要依賴線程池提供的 shutdown() 和 shutdownNow() 方法來終止線程池。不過在使用時須要注意它們的應用場景,尤爲是在使用 shutdownNow() 的時候,必定要謹慎。

相關文章
相關標籤/搜索