『中斷技術』其實是計算機系統中很重要的一個概念,甚至有人說,我們的作業系統就是「中斷驅動的」。
中斷,其實指的就是程式在執行過程中,發生了某些非正常的事件指示當前程式不能繼續執行了,應當得到暫停或終止,而通知正在執行的程式暫停執行的這個操作就叫『中斷』。
中斷同時也是我們實現併發的基礎,中斷一個執行緒的執行,排程另一個執行緒的執行。
中斷源
如果按照中斷事件型別來分,大致上有以下幾種型別的中斷事件型別:
- 機器故障中斷事件。往往是電源故障、硬體裝置連線故障等
- 程式性中斷事件。這種大多是我們的程式程式碼邏輯問題,導致的例如記憶體溢位、除數為零等問題
- 外部中斷事件。主要是時鐘中斷
- 輸入輸出中斷事件。裝置出錯或是傳輸結束
每一種型別的中斷事件都對應一位二進位制的位元位,系統中也對應一箇中斷暫存器用於儲存當前系統所遇到的所有中斷事件,1 表示該型別的中斷事件發生,0 表示未發生。
中斷操作主要分為兩種方式,一種叫『搶佔式中斷』,一種叫『主動式中斷』。前者就是在發生中斷時,強制剝奪執行緒的 CPU,後者是在正在執行的執行緒中斷位上標記一下,具體什麼時候中斷由執行緒自己來決定。
當執行緒發現自己有中斷事件時,會根據中斷事件的型別去對應相應的中斷處理程式來處理該中斷事件。
下面我們看幾種型別的中斷事件,對應的中斷處理程式是如何處理的。
1、電源故障(掉電)
首先,當我們的系統丟失電源時,系統硬裝置是能保證繼續工作一小段時間的。這也是為什麼你的用瀏覽器瀏覽這好幾個標籤,突然關機了,開機後開啟瀏覽器會提示你上次異常關閉,問你是否恢復的原因。
而我們的中斷處理程式首先會將當前所有暫存器中的資料經由主存儲存到磁碟,接著停止 CPU 的執行,直至停機。
下次開機時,中斷處理程式會從磁碟載入中斷前的暫存器資料,恢復現場。
2、程式邏輯中斷
當我們的 CPU 執行除運算時遇到除數為零,將產生一箇中斷事件,對應的處理程式會簡單的將錯誤型別及資訊進行一個返回。
記憶體溢位異常也是一樣的處理。
中斷執行緒
Java API 中執行緒相關的方法主要有三個:
public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()
interrupt 方法表示中斷當前執行緒,僅僅設定一下執行緒的中斷標記位。interrupted 是一個靜態的方法,它將返回當前執行緒的中斷位是否被標記,如果是則返回 true 並清空中斷標記位,否則返回 false。
isInterrupted 方法功能是類似於 interrupted 方法的,只不過無論當前執行緒是否被中斷了,都不會清空中斷標誌位。
我們看一個例子:
public void test() {
Thread thread = new Thread(){
@Override
public void run(){
for (int i=0; i<50000; i++){
System.out.println("i=" + i);
}
}
};
thread.start();
thread.interrupt();
thread.join();
}
複製程式碼
這樣一段程式碼,我們建立一個執行緒,該執行緒啟動後列印 50000 個數字,但是我們的主執行緒中又會去中斷該執行緒。
搶斷式中斷方式下,thread 執行緒可能只列印了幾個數字,甚至還未開始執行列印操作就被剝奪了 CPU,提前結束生命週期。
而我們的 Java 中不推薦使用搶斷式中斷,倡導「一個執行緒的生命不應該由其他執行緒終止,應當由它自己選擇是否停止」。所以,這段程式會成功列印 50000 個數字,即便 thread 執行緒的中斷標記位已經被標記。
簡單修改下,我們的程式碼即能響應中斷:
每一次列印前都去檢查一下自己的中斷標記位是否為 true,判斷自己是否被中斷以採取相應的處理操作。
但是這僅僅是執行緒處於 RUNNABLE 狀態下對於中斷請求的響應情況,下面我們具體看看執行緒的其他狀態下,面對中斷請求的響應措施。
執行緒對於中斷的響應
RUNNABLE
狀態為 RUNNABLE 的執行緒是擁有 CPU 正在執行的執行緒,我們的 interrupt 方法僅僅會設定一下該執行緒的中斷標誌位,不會做任何其他的操作,關於你是否響應此中斷,由你自己決定。
這一型別的程式碼,我們已經在上文介紹了,此處不再贅述了。
WAITING
WAITING 狀態是執行緒在獲得鎖的前提下,正常執行過程中由於缺失一些條件而被迫釋放鎖,交出 CPU,阻塞到等待佇列上,等待別人喚醒的一個狀態。
這個狀態下的執行緒一旦被別人 interrupt 中斷,將直接丟擲異常 java.lang.InterruptedException。我們看一段程式碼:
public void test1() {
Object obj = new Object();
Thread thread = new Thread(){
@Override
public void run(){
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
//主執行緒等待 thread 執行緒獲取 obj 物件鎖並阻塞自己到等待佇列
Thread.sleep(2000);
thread.interrupt();
}
複製程式碼
程式直接丟擲異常,並清空中斷標誌位。
你可以思考一下,一個 WAITING 狀態的執行緒被中斷為什麼要丟擲一個異常?
其實還是那個理念,「任何執行緒都沒有權利終止另一個執行緒的生命」,一個正在 WAITING 中的執行緒由於不具有 CPU 的使用權,你中斷它,它永遠都不會知道自己被中斷了直到自己重新競爭到了鎖並得到執行。
那麼,我們的主執行緒在呼叫 interrupt 方法中斷一個執行緒,當發現它的狀態為 WAITING 時,將喚醒它並更改指令暫存器的值以指向異常程式碼塊,期待你自己來處理這個中斷。
這也是為什麼 wait、sleep、join 這些方法必須處理一個受檢查的異常 InterruptException 的原因,因為這些方法會阻塞執行緒,而如果在阻塞期間收到中斷,你也應當提供中斷的處理邏輯。
BLOCKED
BLOCKED 狀態的執行緒往往是競爭某個鎖失敗,而阻塞在某個物件的阻塞佇列上的執行緒。
這個狀態的執行緒和 RUNNABLE 狀態的執行緒一樣,對於中斷請求不做額外響應,僅僅設定一下中斷標誌位,具體什麼時候處理中斷需要程式自己去迴圈檢測判斷。
NEW/TERMINATE
對於這兩個狀態的執行緒進行中斷請求,目標執行緒什麼也不會做,就連中斷標誌位也不會被設定,因為 Java 認為,一個還未啟動的執行緒和一個已經結束的執行緒,對於他們的中斷是毫無意義的。
總結一下,以上就是我們『中斷技術』的相關概念,它是一種執行緒間協作方式,理解它是為了更優雅的結束執行緒,使程式走在我們的預期之中。