前言
以前有一個錯誤的認識,以為中斷操作都會丟擲異常,後來才發現並不是這樣,所以今天就來做一個關於中斷的總結。
如何關閉執行緒
已被棄用的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()方法即可。