- 相關(guān)推薦
淺談Java線程中斷的本質(zhì)深入理解
一、Java中斷的現(xiàn)象
首先,看看Thread類里的幾個(gè)方法:
public static booleaninterrupted測(cè)試當(dāng)前線程是否已經(jīng)中斷。線程的中斷狀態(tài)由該方法清除。換句話說(shuō),如果連續(xù)兩次調(diào)用該方法,則第二次調(diào)用將返回 false(在第一次調(diào)用已清除了其中斷狀態(tài)之后,且第二次調(diào)用檢驗(yàn)完中斷狀態(tài)前,當(dāng)前線程再次中斷的情況除外)。public booleanisInterrupted()測(cè)試線程是否已經(jīng)中斷。線程的中斷狀態(tài)不受該方法的影響。public voidinterrupt()中斷線程。
上面列出了與中斷有關(guān)的幾個(gè)方法及其行為,可以看到interrupt是中斷線程。如果不了解Java的中斷機(jī)制,這樣的一種解釋極容易造成誤解,認(rèn)為調(diào)用了線程的interrupt方法就一定會(huì)中斷線程。
其實(shí),Java的中斷是一種協(xié)作機(jī)制。也就是說(shuō)調(diào)用線程對(duì)象的interrupt方法并不一定就中斷了正在運(yùn)行的線程,它只是要求線程自己在合適的時(shí)機(jī)中斷自己。每個(gè)線程都有一個(gè)boolean的中斷狀態(tài)(不一定就是對(duì)象的屬性,事實(shí)上,該狀態(tài)也確實(shí)不是Thread的字段),interrupt方法僅僅只是將該狀態(tài)置為true
復(fù)制代碼 代碼如下:
public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已調(diào)用線程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num = longTimeRunningNonInterruptMethod(2, 0);
System.out.println("長(zhǎng)時(shí)間任務(wù)運(yùn)行結(jié)束,num=" + num);
System.out.println("線程的中斷狀態(tài):" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) {
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
}
return initNum;
}
}
}
一般情況下,會(huì)打印如下內(nèi)容:
已調(diào)用線程的interrupt方法
長(zhǎng)時(shí)間任務(wù)運(yùn)行結(jié)束,num=-2
線程的中斷狀態(tài):true
可見(jiàn),interrupt方法并不一定能中斷線程。但是,如果改成下面的程序,情況會(huì)怎樣呢?
復(fù)制代碼 代碼如下:
import java.util.concurrent.TimeUnit;
public class TestInterrupt {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
t.interrupt();
System.out.println("已調(diào)用線程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num = -1;
try {
num = longTimeRunningInterruptMethod(2, 0);
} catch (InterruptedException e) {
System.out.println("線程被中斷");
throw new RuntimeException(e);
}
System.out.println("長(zhǎng)時(shí)間任務(wù)運(yùn)行結(jié)束,num=" + num);
System.out.println("線程的中斷狀態(tài):" + Thread.interrupted());
}
private static int longTimeRunningInterruptMethod(int count, int initNum) throws InterruptedException{
for(int i=0; i<count; i++) {
TimeUnit.SECONDS.sleep(5);
}
return initNum;
}
}
}
經(jīng)運(yùn)行可以發(fā)現(xiàn),程序拋出異常停止了,run方法里的后兩條打印語(yǔ)句沒(méi)有執(zhí)行。那么,區(qū)別在哪里?
一般說(shuō)來(lái),如果一個(gè)方法聲明拋出InterruptedException,表示該方法是可中斷的(沒(méi)有在方法中處理中斷卻也聲明拋出InterruptedException的除外),也就是說(shuō)可中斷方法會(huì)對(duì)interrupt調(diào)用做出響應(yīng)(例如sleep響應(yīng)interrupt的操作包括清除中斷狀態(tài),拋出InterruptedException),如果interrupt調(diào)用是在可中斷方法之前調(diào)用,可中斷方法一定會(huì)處理中斷,像上面的例子,interrupt方法極可能在run未進(jìn)入sleep的時(shí)候就調(diào)用了,但sleep檢測(cè)到中斷,就會(huì)處理該中斷。如果在可中斷方法正在執(zhí)行中的時(shí)候調(diào)用interrupt,會(huì)怎么樣呢?這就要看可中斷方法處理中斷的時(shí)機(jī)了,只要可中斷方法能檢測(cè)到中斷狀態(tài)為true,就應(yīng)該處理中斷。讓我們?yōu)殚_(kāi)頭的那段代碼加上中斷處理。
那么自定義的可中斷方法該如何處理中斷呢?那就是在適合處理中斷的地方檢測(cè)線程中斷狀態(tài)并處理。
復(fù)制代碼 代碼如下:
public class TestInterrupt {
public static void main(String[] args) throws Exception {
Thread t = new MyThread();
t.start();
// TimeUnit.SECONDS.sleep(1);//如果不能看到處理過(guò)程中被中斷的情形,可以啟用這句再看看效果
t.interrupt();
System.out.println("已調(diào)用線程的interrupt方法");
}
static class MyThread extends Thread {
public void run() {
int num;
try {
num = longTimeRunningNonInterruptMethod(2, 0);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("長(zhǎng)時(shí)間任務(wù)運(yùn)行結(jié)束,num=" + num);
System.out.println("線程的中斷狀態(tài):" + Thread.interrupted());
}
private static int longTimeRunningNonInterruptMethod(int count, int initNum) throws InterruptedException {
if(interrupted()) {
throw new InterruptedException("正式處理前線程已經(jīng)被請(qǐng)求中斷");
}
for(int i=0; i<count; i++) {
for(int j=0; j<Integer.MAX_VALUE; j++) {
initNum ++;
}
//假如這就是一個(gè)合適的地方
if(interrupted()) {
//回滾數(shù)據(jù),清理操作等
throw new InterruptedException("線程正在處理過(guò)程中被中斷");
}
}
return initNum;
}
}
}
如上面的代碼,方法longTimeRunningMethod此時(shí)已是一個(gè)可中斷的方法了。在進(jìn)入方法的時(shí)候判斷是否被請(qǐng)求中斷,如果是,就不進(jìn)行相應(yīng)的處理了;處理過(guò)程中,可能也有合適的地方處理中斷,例如上面最內(nèi)層循環(huán)結(jié)束后。
這段代碼中檢測(cè)中斷用了Thread的靜態(tài)方法interrupted,它將中斷狀態(tài)置為false,并將之前的狀態(tài)返回,而isInterrupted只是檢測(cè)中斷,并不改變中斷狀態(tài)。一般來(lái)說(shuō),處理過(guò)了中斷請(qǐng)求,應(yīng)該將其狀態(tài)置為false。但具體還要看實(shí)際情形。
二、Java中斷的本質(zhì)
在歷史上,Java試圖提供過(guò)搶占式限制中斷,但問(wèn)題多多,例如已被廢棄的Thread.stop、Thread.suspend和 Thread.resume等。另一方面,出于Java應(yīng)用代碼的健壯性的考慮,降低了編程門檻,減少不清楚底層機(jī)制的程序員無(wú)意破壞系統(tǒng)的概率。
如今,Java的線程調(diào)度不提供搶占式中斷,而采用協(xié)作式的中斷。其實(shí),協(xié)作式的中斷,原理很簡(jiǎn)單,就是輪詢某個(gè)表示中斷的標(biāo)記,我們?cè)谌魏纹胀ùa的中都可以實(shí)現(xiàn)。 例如下面的代碼:
復(fù)制代碼 代碼如下:
volatile bool isInterrupted;
//…
while(!isInterrupted) {
compute();
}
但是,上述的代碼問(wèn)題也很明顯。當(dāng)compute執(zhí)行時(shí)間比較長(zhǎng)時(shí),中斷無(wú)法及時(shí)被響應(yīng)。另一方面,利用輪詢檢查標(biāo)志變量的方式,想要中斷wait和sleep等線程阻塞操作也束手無(wú)策。
如果仍然利用上面的思路,要想讓中斷及時(shí)被響應(yīng),必須在虛擬機(jī)底層進(jìn)行線程調(diào)度的對(duì)標(biāo)記變量進(jìn)行檢查。是的,JVM中確實(shí)是這樣做的。下面摘自java.lang.Thread的源代碼:
復(fù)制代碼 代碼如下:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
//…
private native boolean isInterrupted(boolean ClearInterrupted);
可以發(fā)現(xiàn),isInterrupted被聲明為native方法,取決于JVM底層的實(shí)現(xiàn)。
實(shí)際上,JVM內(nèi)部確實(shí)為每個(gè)線程維護(hù)了一個(gè)中斷標(biāo)記。但應(yīng)用程序不能直接訪問(wèn)這個(gè)中斷變量,必須通過(guò)下面幾個(gè)方法進(jìn)行操作:
復(fù)制代碼 代碼如下:
public class Thread {
//設(shè)置中斷標(biāo)記
public void interrupt() { ... }
//獲取中斷標(biāo)記的值
public boolean isInterrupted() { ... }
//清除中斷標(biāo)記,并返回上一次中斷標(biāo)記的值
public static boolean interrupted() { ... }
...
}
通常情況下,調(diào)用線程的interrupt方法,并不能立即引發(fā)中斷,只是設(shè)置了JVM內(nèi)部的中斷標(biāo)記。因此,通過(guò)檢查中斷標(biāo)記,應(yīng)用程序可以做一些特殊操作,也可以完全忽略中斷。
你可能想,如果JVM只提供了這種簡(jiǎn)陋的中斷機(jī)制,那和應(yīng)用程序自己定義中斷變量并輪詢的方法相比,基本也沒(méi)有什么優(yōu)勢(shì)。
JVM內(nèi)部中斷變量的主要優(yōu)勢(shì),就是對(duì)于某些情況,提供了模擬自動(dòng)“中斷陷入”的機(jī)制。
在執(zhí)行涉及線程調(diào)度的阻塞調(diào)用時(shí)(例如wait、sleep和join),如果發(fā)生中斷,被阻塞線程會(huì)“盡可能快的”拋出InterruptedException。因此,我們就可以用下面的代碼框架來(lái)處理線程阻塞中斷:
復(fù)制代碼 代碼如下:
try {
//wait、sleep或join
}
catch(InterruptedException e) {
//某些中斷處理工作
}
所謂“盡可能快”,我猜測(cè)JVM就是在線程調(diào)度調(diào)度的間隙檢查中斷變量,速度取決于JVM的實(shí)現(xiàn)和硬件的性能。
三、一些不會(huì)拋出 InterruptedException 的線程阻塞操作
然而,對(duì)于某些線程阻塞操作,JVM并不會(huì)自動(dòng)拋出InterruptedException異常。例如,某些I/O操作和內(nèi)部鎖操作。對(duì)于這類操作,可以用其他方式模擬中斷:
1)java.io中的異步socket I/O
讀寫socket的時(shí)候,InputStream和OutputStream的read和write方法會(huì)阻塞等待,但不會(huì)響應(yīng)java中斷。不過(guò),調(diào)用Socket的close方法后,被阻塞線程會(huì)拋出SocketException異常。
2)利用Selector實(shí)現(xiàn)的異步I/O
如果線程被阻塞于Selector.select(在java.nio.channels中),調(diào)用wakeup方法會(huì)引起ClosedSelectorException異常。
3)鎖獲取
如果線程在等待獲取一個(gè)內(nèi)部鎖,我們將無(wú)法中斷它。但是,利用Lock類的lockInterruptibly方法,我們可以在等待鎖的同時(shí),提供中斷能力。
四、兩條編程原則
另外,在任務(wù)與線程分離的框架中,任務(wù)通常并不知道自身會(huì)被哪個(gè)線程調(diào)用,也就不知道調(diào)用線程處理中斷的策略。所以,在任務(wù)設(shè)置了線程中斷標(biāo)記后,并不能確保任務(wù)會(huì)被取消。因此,有以下兩條編程原則:
1)除非你知道線程的中斷策略,否則不應(yīng)該中斷它。
這條原則告訴我們,不應(yīng)該直接調(diào)用Executer之類框架中線程的interrupt方法,應(yīng)該利用諸如Future.cancel的方法來(lái)取消任務(wù)。
2)任務(wù)代碼不該猜測(cè)中斷對(duì)執(zhí)行線程的含義。
這條原則告訴我們,一般代碼遇在到InterruptedException異常時(shí),不應(yīng)該將其捕獲后“吞掉”,而應(yīng)該繼續(xù)向上層代碼拋出。
總之,Java中的非搶占式中斷機(jī)制,要求我們必須改變傳統(tǒng)的搶占式中斷思路,在理解其本質(zhì)的基礎(chǔ)上,采用相應(yīng)的原則和模式來(lái)編程。
【淺談Java線程中斷的本質(zhì)深入理解】相關(guān)文章:
深入理解java的反射04-02
java的多線程12-04
java多線程03-27
最新的Java容器類的深入理解04-02
java語(yǔ)言的多線程11-25
java線程的幾種狀態(tài)12-14
淺談理解Java中的弱引用04-02