【雜談】執行緒中斷——Interrupt

貓毛·波拿巴發表於2018-11-28

前言

  以前有一個錯誤的認識,以為中斷操作都會丟擲異常,後來才發現並不是這樣,所以今天就來做一個關於中斷的總結。

如何關閉執行緒  

已被棄用的Stop方法  

  早期,Thread類中有一個stop方法,用於強行關閉一個執行緒。但是後來發現此操作並不安全,強行關閉可能導致一致性問題。故stop方法已被官方棄用。具體原因請看Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?.。

  既然,stop方法不能用了,我們就要另闢蹊徑。而我們知道,只要執行緒的run方法"完成",底層作業系統的執行緒就會被釋放。"完成"意味著,run方法結束,而結束的方式有兩種,一種是丟擲異常,另一種則是方法返回。所以,我們要做的就是控制執行緒任務,丟擲異常或返回(return)。

Flag機制

  前面說到對執行緒任務的執行進行控制,而執行緒任務一旦跑起來,又如何對其執行情況進行干預呢?答案就是,讓程式碼反覆檢查某個值的狀態,如果達到某個狀態,就結束執行(return)或者丟擲異常。而這個值,可以被外部訪問到,使用者可以在需要的時候設定"結束Flag"。

例如:

class Task implements Runnable {
    private volatile boolean stop = false;

    public void stop() {
        stop = true;
    }

    public void run() {
        while(!stop) { //每執行一次操作,檢查一次狀態
            System.out.println("I'm running...");
        }
        System.out.println("I'm stopped.");
    }
}

public class Main {
    public void main(String[] args) throws InterruptedException{
        Task task = new Task();
        Thread t = new Thread(task);
        t.start(); //啟動任務執行緒
        Thread.sleep(3*1000); //主執行緒休眠3秒
        task.stop();//三秒後關閉任務執行緒
    }
}

 

中斷機制

  強制結束執行緒被取消,取而代之的,是一種協作機制,即中斷機制。也就是說,一個執行緒只能向另一個執行緒傳送中斷訊號,而不能強行對其進行關閉。而另一個執行緒如何處理,就得看其正在執行的程式碼對中斷訊號如何反應了。其實就是前面說的Flag機制,但是,其內部維護Flag,還有額外的和阻塞庫互動的內容。例子如下:

class Task implements Runnable {
    public void run() {
        while(!Thread.currentThread().interrupted()){ //檢查執行緒中斷狀態
            System.out.println("I'm running...");
        }
        System.out.println("I'm stopped.");
    }
}

public class Main {
    public void main(String[] args) throws InterruptedException{
        Task task = new Task();
        Thread t = new Thread(task);
        t.start(); //啟動任務執行緒
        Thread.sleep(3*1000); //主執行緒休眠3秒
        t.interrupt();//三秒後中斷任務執行緒
    }
}

其中,Thread.currentThread()靜態方法將獲得當前執行此段程式碼的執行緒物件。然後呼叫interrupted()方法獲取執行緒中斷狀態,如果執行緒被中斷,則返回true。執行緒的中斷狀態可以用interrupt()方法設定。

下面提一下中斷可能產生讓人產生疑惑的幾個方法:

  • interrupt() => 設定中斷狀態,設定為已中斷
  • isInterrupted() => 獲取中斷狀態
  • interrupted() => 恢復中斷狀態,並返回恢復前的狀態。(即如果被中斷,會設定為未中斷,並返回true)

interrupt方法到底做了什麼

  要想查明原因,最好的方法就是檢視原始碼。我們先來看看interrupt程式碼

    

  其中,同步塊可以暫時無視,因為那是跟I/O操作掛鉤的I/O中斷器,在進行I/O操作是,對應類庫會對其進行設定。那麼剩下的就只有一個方法interrupt0()。顯然這是一個本地方法。那我們就看看JVM中,這個方法的實現。先來看看與interrupt0關聯的方法是什麼。線上檢視連結(牆外)

  

  由上述關聯方法可知,本地方法的實現依賴於JVM實現。下面內容以Hospot虛擬機器為例。以下來自hotspot原始碼\src\share\vm\prims\jvm.cpp

  

  前面只是做一些狀態檢查,最主要的是呼叫Thread::interrupt函式。以下來自hospot原始碼\src\share\vm\runtime\thread.cpp

  

  此處呼叫os,即作業系統的中斷方法。以下來自hospot原始碼\src\os\linux\vm\os_linux.cpp

  

  以上主要進行了兩個操作,一個設定中斷狀態,一個是喚醒當前執行緒(如果當前執行緒處於掛起狀態)。

為何需要喚醒執行緒?

   前面已經說了,中斷跟Flag機制差不多,不會實際上關閉執行緒。要想關閉執行緒,必須讓執行緒方法返回或者丟擲異常。如果不喚醒執行緒,則程式碼也就不會被執行。任務會一直處於無法完成的狀態。

Wait()、Sleep()與中斷異常

   如果wait或sleep方法被中斷,會丟擲中斷異常。這就是前面說的與阻塞庫的互動內容。但是我們已經看到,中斷操作實際上只是設定了中斷狀態,和喚醒執行緒。那麼異常丟擲又是在哪裡實現的呢?而且wait和sleep方法同樣是本地方法,那沒辦法了,只能再看原始碼,以下以sleep方法為例。以下來自hotspot原始碼\src\share\vm\prims\jvm.cpp

  

  再來看看os::sleep裡面是如何處理的。以下來自hotspot原始碼\src\os\linux\vm\os_linux.cpp

  

  由以上程式碼可知,sleep函式會將執行緒掛起,然後程式碼卡死在掛起操作上,然後其他執行緒中斷了此執行緒,此執行緒被喚醒,然後sleep函式繼續執行,檢查中斷狀態,如果已中斷,則丟擲中斷異常。

為何要恢復中斷狀態

  這就跟執行緒的複用有關了。首先要明確一點的是,打斷執行緒任務和打斷執行緒是兩碼事。我們可以通過中斷打斷執行緒任務,線上程池的案例中,此任務結束後,執行緒會繼續獲取並執行其他任務,並沒有因為任務的中斷而關閉執行緒。也就是說,當前執行緒完成了當前的任務,會繼續完成其他任務。那這時候如果執行緒還保留著中斷狀態,就會對後續的任務執行有影響。影響何在呢?看看上面的程式碼,有沒有注意到,sleep函式在掛起執行緒之前會先檢查執行緒的中斷狀態。如果執行緒處於中斷狀態,則直接丟擲中斷異常。wait函式也是同理。那這樣的話,其他任務就無法掛起或休眠此執行緒了。因此,我們在檢查中斷狀態的時候,還需將其中斷狀態恢復,即執行interrupted()方法即可。

 

相關文章