Two Phase Termination 設計模式:兩階段終止模式

話題:兩階段終止模式:如何優雅地終止線程?

啓動多線程去執行一個異步任務。既啓動,那又該如何終止呢?今天咱們就從技術的角度聊聊如何優雅地終止線程,正所謂有始有終。

線程執行完或者出現異常就會進入終止狀態。這樣看,終止一個線程看上去很簡單啊!一個線程執行完自己的任務,自己進入終止狀態,這的確很簡單。不過我們今天談到的“優雅地終止線程”,不是自己終止自己,而是在一個線程T1中,終止線程T2;這裏所謂的“優雅”,指的是給T2一個機會料理後事,而不是被一劍封喉。

Java語言的Thread類中曾經提供了一個stop()方法,用來終止線程,可是早已不建議使用了,原因是這個方法用的就是一劍封喉的做法,被終止的線程沒有機會料理後事。

既然不建議使用stop()方法,那在Java領域,我們又該如何優雅地終止線程呢?

如何理解兩階段終止模式

前輩們經過認真對比分析,已經總結出了一套成熟的方案,叫做兩階段終止模式。顧名思義,就是將終止過程分成兩個階段,其中第一個階段主要是線程T1向線程T2發送終止指令,而第二階段則是線程T2響應終止指令
在這裏插入圖片描述
那在Java語言裏,終止指令是什麼呢?這個要從Java線程的狀態轉換過程說起。之前曾經提到過Java線程的狀態轉換圖,如下圖所示。
在這裏插入圖片描述
在這裏插入圖片描述
從這個圖裏你會發現,Java線程進入終止狀態的前提是線程進入RUNNABLE狀態,而實際上線程也可能處在休眠狀態,也就是說,我們要想終止一個線程,首先要把線程的狀態從休眠狀態轉換到RUNNABLE狀態。如何做到呢?這個要靠Java Thread類提供的interrupt()方法,它可以將休眠狀態的線程轉換到RUNNABLE狀態。

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

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

理解了兩階段終止模式之後,下面我們看一個實際工作中的案例。

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

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

在這裏插入圖片描述
下面的示例代碼是監控代理簡化之後的實現,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();
  }
}

如何優雅地終止線程池
Java領域用的最多的還是線程池,而不是手動地創建線程。那我們該如何優雅地終止線程池呢?

線程池提供了兩個方法:shutdown()和shutdownNow()。這兩個方法有什麼區別呢?要了解它們的區別,就先需要了解線程池的實現原理。

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

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

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

如果提交到線程池的任務不允許取消,那就不能使用shutdownNow()方法終止線程池。不過,如果提交到線程池的任務允許後續以補償的方式重新執行,也是可以使用shutdownNow()方法終止線程池的。《Java併發編程實戰》這本書第7章《取消與關閉》的“shutdownNow的侷限性”一節中,提到一種將已提交但尚未開始執行的任務以及已經取消的正在執行的任務保存起來,以便後續重新執行的方案,你可以參考一下,方案很簡答,這裏就不詳細介紹了。

其實分析完shutdown()和shutdownNow()方法你會發現,它們實質上使用的也是兩階段終止模式,只是終止指令的範圍不同而已,前者隻影響阻塞隊列接收任務,後者範圍擴大到線程池中所有的任務。
在這裏插入圖片描述
在這裏插入圖片描述

總結

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

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

Demo1

public class CounterTest {//多線程Two Phase Termination設計模式    2 階段 終止
    public static void main(String[] args) throws InterruptedException {
        CounterIncrement counterIncrement = new CounterIncrement();
        counterIncrement.start();

        Thread.sleep(10_000L);
        counterIncrement.close();
    }
}

public class CounterIncrement extends Thread {

    private volatile boolean terminated = false;

    private int counter = 0;

    private Random random = new Random(System.currentTimeMillis());

    @Override
    public void run() {
        try {
            while (!terminated) {
                System.out.println(Thread.currentThread().getName() + " " + counter++);
                Thread.sleep(random.nextInt(1000));
            }
        } catch (InterruptedException e) {
            //e.printStackTrace();
        } finally {
            this.clean();
        }
    }

    private void clean() {
        System.out.println("do some clean work for the second phase,current counter " + counter);
    }

    public void close() {
        this.terminated = true;
        this.interrupt();
    }
}

1
public class AppServerClient {
    public static void main(String[] args) throws InterruptedException, IOException {
        AppServer server = new AppServer(13345);
        server.start();
        Thread.sleep(15_000L);
        server.shutdown();
    }
}
2
public class AppServer extends Thread {

    private final int port;

    private static final int DEFAULT_PORT = 12722;

    private volatile boolean start = true;

    private List<ClientHandler> clientHandlers = new ArrayList<>();

    private final ExecutorService executor = Executors.newFixedThreadPool(10);

    private ServerSocket server;

    public AppServer() {
        this(DEFAULT_PORT);
    }

    public AppServer(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        try {
            this.server = new ServerSocket(port);
            while (start) {
                //接受請求
                Socket client = server.accept();
                //將socket再次封裝成ClientHandler對象
                ClientHandler clientHandler = new ClientHandler(client);
                executor.submit(clientHandler);
                this.clientHandlers.add(clientHandler);
            }
        } catch (IOException e) {
//            throw new RuntimeException(e);
        } finally {
            //像這種資源的東西都不要忘記資源的釋放
            this.dispose();
        }
    }

    private void dispose() {
        System.out.println("dispose");
        this.clientHandlers.stream().forEach(ClientHandler::stop);
        this.executor.shutdown();
    }

    public void shutdown() throws IOException {
        this.start = false;
        this.interrupt();
        this.server.close();
    }
}
3
public class ClientHandler implements Runnable {

    private final Socket socket;

    private volatile boolean running = true;

    public ClientHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream();
             BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
             PrintWriter printWriter = new PrintWriter(outputStream)) {
            while (running) {
                String message = br.readLine();
                if (message == null)
                    break;
                System.out.println("Come from client >" + message);
                printWriter.write("echo " + message + "\n");
                printWriter.flush();
            }
        } catch (IOException e) {
//            e.printStackTrace();
            this.running = false;
        } finally {
            this.stop();
        }
    }

    public void stop() {
        if (!running) {
            return;
        }

        this.running = false;

        try {
            this.socket.close();
        } catch (IOException e) {
            //
        }
    }
}

Demo2

1
public class ThreadCloseGraceful {//設置標誌位的方式結束線程的生命週期

    private static class Worker extends Thread {
        private volatile boolean start = true;

        @Override
        public void run() {
            while (start) {
                //
            }
        }

        public void shutdown() {
            this.start = false;
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.start();

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        worker.shutdown();
    }
}
2
public class ThreadCloseGraceful2 {//採用優雅的方式結束線程生命週期
    private static class Worker extends Thread {

        @Override
        public void run() {
            while (true) {

                //方式2:用線程自帶的打斷標記位進行判斷
                System.out.println("Before Thread.interrupted() invoke: " + this.isInterrupted());//false
                if (Thread.interrupted()) {//該api會清除標誌位的flag,上下2句打印狀態都是false 靜態方法
                    System.out.println("After Thread.interrupted() invoke: " + this.isInterrupted());//false  實例方法
                    break;
                }


                //方式1:用小睡一下然會在外面打斷它即可
                //有await,sleep,join等方法打斷是會有異常
                try {
                    System.out.println("Before invoke: " + this.isInterrupted());//false
                    sleep(100);//sleep方法由於中斷而拋出異常之後,線程的中斷標誌會被清除(置爲false),所以這裏打印都是false
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //這裏就可以優雅的方式結束線程生命週期
                    System.out.println("After invoke: " + this.isInterrupted());//false
                    break;
                }



                //第三種情況下:特殊情況下,如果在connection阻塞了,
                // 沒有機會讀標記位,沒機會去對線程的標記位進行判斷,即方式1和2在這裏就不行了
                //就可以強制的關閉它,第17講解
                //readFile  在這裏阻塞了,怎麼處理???暴力的方法解決ThreadService對原生的Thread進行包裝一下
            }


            //後面還有邏輯
            //-------------
            //-------------
            //-------------
        }
    }

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.start();

        try {
            //等待worker線程工作
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //規定的時間內沒有完成就打斷它
        worker.interrupt();
    }
}

1
public class ThreadCloseForce {

    public static void main(String[] args) {

        ThreadService service = new ThreadService();
        long start = System.currentTimeMillis();

        service.execute(() -> {
            //集羣之間拷貝文件,時間之內沒完成就停止它,IO操作超時的巧妙結束線程的生命週期
            //load a very heavy resource.
            /*while (true) {

            }*/
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        //任務最多執行的時間
        service.shutdown(5000);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
}
2
public class ThreadService {//對一般的Thread進行了包裝

    private Thread executeThread;//執行線程

    private volatile boolean finished = false;

    //執行,讓守護線程去做
    public void execute(Runnable task) {

        executeThread = new Thread() {
            @Override
            public void run() {
                //裏面設置一個守護線程幹活
                Thread runner = new Thread(task);
                //執行線程退出了,守護線程也退出了,讓守護線程執行,
                runner.setDaemon(true);

                //啓動守護線程
                runner.start();
                //可能守護線程來不起啓動去幹活就死掉了,所以在下面要在executeThread上join,等待守護線程執行完

                try {
                    //執行線程(executeThread)在這裏阻塞住了
                    runner.join();
                    finished = true;
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
            }
        };

        executeThread.start();
    }

    //關閉,設置最多執行的時間
    public void shutdown(long mills) {
        long currentTime = System.currentTimeMillis();
        while (!finished) {
            if ((System.currentTimeMillis() - currentTime) >= mills) {
                System.out.println("任務超時,需要結束他!");
                executeThread.interrupt();//打斷後執行線程(executeThread)就執行完了,它的生命週期結束了,開啓的守護線程也就死了
                break;
            }

            //既沒有超時也沒用執行結束,短暫的休眠一下
            try {
                executeThread.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("執行線程被打斷!");
                break;
            }
        }

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