爲什麼說volatile+interrupt是停止線程最優雅的姿勢?

使用stop方法

調用stop方法,會讓正在運行的線程直接中止,有可能會讓一些清理性的工作得不到完成。並且stop已經被標記爲廢棄的方法,不建議使用。

正確的使用姿勢是使用兩階段終止的模式,即一個線程發送終止指令,另一個線程接收指令,並且決定自己在何時停止。

使用標誌位

public class RunTask {

    private volatile boolean stopFlag;
    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!stopFlag) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        stopFlag = true;
    }
}

「stopFlag上加volatile是保證可見性。我這個例子用了while循環不斷判斷,如果項目中用不到while的話,可以在關鍵節點判斷,然後退出run方法即可」

使用interrupt方法

假如我們的任務中有阻塞的邏輯,如調用了Thread.sleep方法,如何讓線程停止呢?

從線程狀態轉換圖中尋找答案從圖中可以看到如果想讓線程進入終止狀態的前提是這個線程處於運行狀態。當我們想要終止一個線程的時候,如果此時線程處於阻塞狀態,我們如何把它轉換到運行狀態呢?

我們可以通過調用Thread#interrupt方法,將阻塞狀態的線程轉換到就緒狀態,進入由操作系統調度成運行狀態,即可終止。

那線程在運行狀態中調用interrupt方法,會發生什麼呢?

public class RunTaskCase1 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (true) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

依次調用start方法和stop方法,發現線程並沒有停止。

「其實當線程處於運行狀態時,interrupt方法只是在當前線程打了一個停止的標記,停止的邏輯需要我們自己去實現」

「Thread類提供瞭如下2個方法來判斷線程是否是中斷狀態」

  1. isInterrupted
  2. interrupted

這2個方法雖然都能判斷狀態,但是有細微的差別

 @Test
 public void testInterrupt() throws InterruptedException {
     Thread thread = new Thread(() -> {
         while (true) {}
     });
     thread.start();
     TimeUnit.MICROSECONDS.sleep(100);
     thread.interrupt();
     // true
     System.out.println(thread.isInterrupted());
     // true
     System.out.println(thread.isInterrupted());
     // true
     System.out.println(thread.isInterrupted());
 }
 @Test
 public void testInterrupt2() {
     Thread.currentThread().interrupt();
     // true
     System.out.println(Thread.interrupted());
     // false
     System.out.println(Thread.interrupted());
     // false
     System.out.println(Thread.interrupted());
 }

「isInterrupted和interrupted的方法區別如下」

Thread#isInterrupted:測試線程是否是中斷狀態,執行後不更改狀態標誌 Thread#interrupted:測試線程是否是中斷狀態,執行後將中斷標誌更改爲false

「所以此時我們不需要自已定義狀態,直接用中斷標誌即可,之前的代碼可以改爲如下」

public class RunTaskCase2 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("doSomething");
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

當線程處於阻塞狀態時,調用interrupt方法,會拋出InterruptedException,也能終止線程的執行

「注意:發生異常時線程的中斷標誌爲會由true更改爲false。」

所以我們有如下實現 當線程處於運行狀態:用自己定義的標誌位來退出 當線程處於阻塞狀態:用拋異常的方式來退出

public class RunTaskCase3 {

    private volatile boolean stopFlag;
    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (stopFlag) {
                try {
                    System.out.println("doSomething");
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        taskThread.start();
    }

    public void stop() {
        stopFlag = true;
        taskThread.interrupt();
    }
}

當然也可以一直用中斷標誌來退出,「注意,當發生異常的時候需要重置中斷標誌位」

public class RunTaskCase4 {

    private Thread taskThread;

    public void start() {
        taskThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("doSomething");
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    // 重置中斷標誌位爲true
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
            }
        });
        taskThread.start();
    }

    public void stop() {
        taskThread.interrupt();
    }
}

最後問大家一個問題?RunTaskCase3和RunTaskCase4哪種實現方式比較好呢?

「雖然RunTaskCase4代碼看起來更簡潔,但是RunTaskCase4不建議使用,因爲如果在run方法中調用了第三方類庫,發生了InterruptedException異常,但是沒有重置中斷標誌位,會導致線程一直運行下去,同理RunTaskCase2也不建議使用」

本文分享自微信公衆號 - Java識堂(erlieStar)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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