在歷史上,Java試圖提供過搶佔式限制中斷,但問題多多,例如前文介紹的已被廢棄的Thread.stop、Thread.suspend和Thread.resume等。另一方面,出於Java應用程式碼的健全性的考慮,降低了程式設計門檻,減少不清楚底層機制的程式設計師無意破壞系統的機率。
如今,Java的執行緒調度不提供搶佔式中斷,而採用協作式的中斷。其實,協作式的中斷,原理很簡單,就是輪詢某個表示中斷的標記,我們在任何普通程式碼的中都可以實現。
例如下面的程式碼:
volatile bool isInterrupted;
//…
while(!isInterrupted) {
compute();
}
但是,上述的程式碼問題也很明顯。當compute執行時間比較長時,中斷無法及時被回應。另一方面,利用輪詢檢查標誌變數的方式,想要中斷wait和sleep等執行緒阻塞操作也束手無策。
如果仍然利用上面的思路,要想讓中斷及時被回應,必須在虛擬機器底層進行線程調度的對標記變數進行檢查。是的,JVM中確實是這樣做的。
下面摘自java.lang.Thread的原始碼:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
//…
private native boolean isInterrupted(boolean ClearInterrupted);
可以發現,isInterrupted被宣告為native方法,取決於JVM底層的實作。
實際上,JVM內部確實為每個執行緒維護了一個中斷標記。但應用程式不能直接存取這個中斷變量,必須透過下面幾個方法進行操作:
public class Thread {
//設定中斷標記
public void interrupt() { ... }
//取得中斷標記的值
public boolean isInterrupted() { ... }
//清除中斷標記,並傳回上一次中斷標記的值
public static boolean interrupted() { ... }
}
通常情況下,呼叫線程的interrupt方法,並不能立即引發中斷,只是設定了JVM內部的中斷標記。因此,透過檢查中斷標記,應用程式可以做一些特殊操作,也可以完全忽略中斷。
你可能會想,如果JVM只提供了這種簡陋的中斷機制,那和應用程式自己定義中斷變數並輪詢的方法相比,基本上也沒有什麼優勢。
JVM內部中斷變數的主要優勢,就是對於某些情況,提供了模擬自動「中斷陷入」的機制。
在執行涉及執行緒調度的阻塞呼叫時(例如wait、sleep和join),如果發生中斷,被阻塞執行緒會「盡可能快的」拋出InterruptedException。因此,我們就可以用下面的程式碼框架來處理執行緒阻塞中斷:
try {
//wait、sleep或join
}
catch(InterruptedException e) {
//某些中斷處理工作
}
所謂“盡可能快”,我猜測JVM是在線程調度調度的間隙檢查中斷變量,速度取決於JVM的實現和硬體的性能。
然而,對於某些執行緒阻塞操作,JVM並不會自動拋出InterruptedException異常。例如,某些I/O操作和內部鎖定操作。對於這類操作,可以用其他方式模擬中斷:
1)java.io中的非同步socket I/O
讀寫socket的時候,InputStream和OutputStream的read和write方法會阻塞等待,但不會回應java中斷。不過,在呼叫Socket的close方法後,被阻塞執行緒會拋出SocketException例外。
2)利用Selector實現的非同步I/O
如果執行緒被阻塞於Selector.select(在java.nio.channels中),呼叫wakeup方法會造成ClosedSelectorException異常。
3)鎖獲取
如果線程在等待獲取一個內部鎖,我們將無法中斷它。但是,利用Lock類別的lockInterruptibly方法,我們可以在等待鎖定的同時,提供中斷能力。
另外,在任務與執行緒分離的框架中,任務通常不知道自身會被哪個執行緒調用,也就不知道呼叫執行緒處理中斷的策略。所以,在任務設定了執行緒中斷標記後,並不能確保任務會被取消。因此,有以下兩個編程原則:
1)除非你知道線程的中斷策略,否則不應該中斷它。
這項原則告訴我們,不應該直接呼叫Executer之類框架中執行緒的interrupt方法,應該利用諸如Future.cancel的方法來取消任務。
2)任務代碼不該猜測中斷對執行緒的意義。
這項原則告訴我們,一般程式碼遇在InterruptedException異常時,不應該將其捕獲後“吞掉”,而應該繼續向上層程式碼拋出。
總之,Java中的非搶佔式中斷機制,要求我們必須改變傳統的搶佔式中斷思路,在理解其本質的基礎上,採用相應的原則和模式來編程。