還不知道如何在java中終止一個執行緒?快來,一文給你揭秘

flydean發表於2023-03-09

簡介

工作中我們經常會用到執行緒,一般情況下我們讓執行緒執行就完事了,那麼你們有沒有想過如何去終止一個正在執行的執行緒呢?

今天帶大家一起來看看。

Thread.stop被禁用之謎

問道怎麼終止一個執行緒,可能大多數人都知道可以呼叫Thread.stop方法。

但是這個方法從jdk1.2之後就不推薦使用了,為什麼不推薦使用呢?

我們先來看下這個方法的定義:

  @Deprecated(since="1.2")
    public final void stop() {
        @SuppressWarnings("removal")
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }
        // A zero status value corresponds to "NEW", it can't change to
        // not-NEW because we hold the lock.
        if (threadStatus != 0) {
            resume(); // Wake up thread if it was suspended; no-op otherwise
        }

        // The VM can handle all thread states
        stop0(new ThreadDeath());
    }

從程式碼我們可以看出,stop這個方法首先檢測有沒有執行緒訪問的許可權。如果有許可權的話,來判斷當前的執行緒是否是剛剛建立的執行緒,如果不是剛剛建立的,那麼就呼叫resume方法來解除執行緒的暫停狀態。

最後呼叫stop0方法來結束執行緒。

其中resume和stop0是兩個native的方法,具體的實現這裡就不講了。

看起來stop方法很合理,沒有什麼問題。那麼為什麼說這個方法是不安全的呢?

接下來我們來看一個例子。

我們建立一個NumberCounter的類,這個類有一個increaseNumber的安全方法,用來對number加一:

public class NumberCounter {
    //要儲存的數字
    private volatile int number=0;
    //數字計數器的邏輯是否完整
    private volatile boolean flag = false;

    public synchronized int increaseNumber() throws InterruptedException {
        if(flag){
            //邏輯不完整
            throw new RuntimeException("邏輯不完整,數字計數器未執行完畢");
        }
        //開始執行邏輯
        flag = true;
        //do something
        Thread.sleep(5000);
        number++;
        //執行完畢
        flag=false;
        return number;
    }
}

事實上,在實際工作中這樣的方法可能需要執行比較久的時間,所以這裡我們透過呼叫Thread.sleep來模擬這個耗時操作。

這裡我們還有一個flag引數,來標誌這個increaseNumber方法是否成功執行完畢。

好了,接下來我們在一個執行緒中呼叫這個類的方法,看看會發生什麼:

    public static void main(String[] args) throws InterruptedException {
        NumberCounter numberCounter= new NumberCounter();
        Thread thread = new Thread(()->{
            while (true){
                try {
                    numberCounter.increaseNumber();
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
            }
        });
        thread.start();
        Thread.sleep(3000);
        thread.stop();
        numberCounter.increaseNumber();
    }

這裡,我們建立了一個執行緒,等這個執行緒執行3秒鐘之後,直接呼叫thread.stop方法,結果我們發現出現了下面的異常:

Exception in thread "main" java.lang.RuntimeException: 邏輯不完整,數字計數器未執行完畢
    at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:12)
    at com.flydean.Main.main(Main.java:18)

這是因為thread.stop方法直接終止了執行緒的執行,導致mberCounter.increaseNumber未執行完畢。

但是這個未執行完畢的狀態是隱藏的,如果使用thread.stop方法來終止執行緒,很有可能導致未知的結果。

所以,我們說thread.stop是不安全的。

怎麼才能安全?

那麼,如果不呼叫thread.stop方法,怎麼才能安全的終止執行緒呢?

所謂安全,那就是需要讓執行緒裡面的邏輯執行完畢,而不是執行一半。

為了實現這個效果,Thread為我們提供了三個比較類似的方法,他們分別是interrupt、interrupted和isInterrupted。

interrupt是給執行緒設定中斷標誌;interrupted是檢測中斷並清除中斷狀態;isInterrupted只檢測中斷。還有重要的一點就是interrupted是類方法,作用於當前執行緒,interrupt和isInterrupted作用於此執行緒,即程式碼中呼叫此方法的例項所代表的執行緒。

interrupt就是中斷的方法,它的工作流程如下:

  1. 如果當前執行緒例項在呼叫Object類的wait(),wait(long)或wait(long,int)方法或join(),join(long),join(long,int)方法,或者在該例項中呼叫了Thread.sleep(long)或Thread.sleep(long,int)方法,並且正在阻塞狀態中時,則其中斷狀態將被清除,並將收到InterruptedException。
  2. 如果此執行緒在InterruptibleChannel上的I/O操作中處於被阻塞狀態,則該channel將被關閉,該執行緒的中斷狀態將被設定為true,並且該執行緒將收到java.nio.channels.ClosedByInterruptException異常。
  3. 如果此執行緒在java.nio.channels.Selector中處於被被阻塞狀態,則將設定該執行緒的中斷狀態為true,並且它將立即從select操作中返回。
  4. 如果上面的情況都不成立,則設定中斷狀態為true。

在上面的例子中,NumberCounter的increaseNumber方法中,我們呼叫了Thread.sleep方法,所以如果在這個時候,呼叫了thread的interrupt方法,執行緒就會丟擲一個InterruptedException異常。

我們把上面呼叫的例子改成下面這樣:

    public static void main(String[] args) throws InterruptedException {
        NumberCounter numberCounter = new NumberCounter();

        Thread thread = new Thread(() -> {
            while (true) {
                try {
                    numberCounter.increaseNumber();
                } catch (InterruptedException e) {
                    System.out.println("捕獲InterruptedException");
                    throw new RuntimeException(e);
                }
            }
        });

        thread.start();
        Thread.sleep(500);
        thread.interrupt();
        numberCounter.increaseNumber();
    }

執行之後再試一次:

Exception in thread "main" Exception in thread "Thread-0" java.lang.RuntimeException: 邏輯不完整,數字計數器未執行完畢
    at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:12)
    at com.flydean.Main2.main(Main2.java:21)
java.lang.RuntimeException: java.lang.thread.interrupt: sleep interrupted
    at com.flydean.Main2.lambda$main$0(Main2.java:13)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interrupted
    at java.base/java.lang.Thread.sleep(Native Method)
    at com.flydean.NumberCounter.increaseNumber(NumberCounter.java:17)
    at com.flydean.Main2.lambda$main$0(Main2.java:10)
    ... 1 more
捕獲InterruptedException

可以看到,我們捕獲到了這個InterruptedException,並且得知具體的原因是sleep interrupted。

捕獲異常之後的處理

從上面的分析可以得知,thread.stop跟thread.interrupt的表現機制是不一樣的。thread.stop屬於悄悄終止,我們程式不知道,所以會導致資料不一致,從而產生一些未知的異常。

而thread.interrupt會顯示的丟擲InterruptedException,當我們捕捉到這個異常的時候,我們就知道執行緒裡面的邏輯在執行的過程中受到了外部作用的干擾,那麼我們就可以執行一些資料恢復或者資料校驗的動作。

在上面的程式碼中,我們是捕獲到了這個異常,列印出異常日誌,然後向上丟擲一個RuntimeException。

正常情況下我們是需要在捕獲異常之後,進行一些處理。

那麼自己處理完這個異常之後,是不是就完美了呢?

答案是否定的。

因為如果我們自己處理了這個InterruptedException, 那麼程式中其他部分如果有依賴這個InterruptedException的話,就可能會出現資料不一致的情況。

所以我們在自己處理完InterruptedException之後,還需要再次丟擲這個異常。

怎麼丟擲InterruptedException異常呢?

有兩種方式,第一種就是在呼叫Thread.interrupted()清除了中斷標誌之後立即丟擲:

   if (Thread.interrupted())  // Clears interrupted status!
       throw new InterruptedException();

還有一種方式就是,在捕獲異常之後,呼叫Thread.currentThread().interrupt()再次中斷執行緒。

public void run () {
  try {
    while (true) {
      // do stuff
    }
  }catch (InterruptedException e) {
    LOGGER.log(Level.WARN, "Interrupted!", e);
    // Restore interrupted state...
    Thread.currentThread().interrupt();
  }
}

這兩種方式都能達到預想的效果。

總結

執行緒不能呼叫stop來終止主要是因為不會丟擲異常,從而導致一些安全和資料不一致的問題。所以,最好的方式就是呼叫interrupt方法來處理。

本文的例子https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/how-to-stop-thread

更多文章請看 www.flydean.com

相關文章