什么時候需要關(guān)閉一個線程咆耿?
下面簡單的舉例情況:
- 比如我們會啟動多個線程做同一件事,比如搶12306的火車票,我們可能開啟多個線程從多個渠道買火車票呆馁,只要有一個渠道買到了,我們會通知取消其他渠道毁兆。這個時候需要關(guān)閉其他線程
- 很多線程的運行模式是死循環(huán)浙滤,比如在生產(chǎn)者/消費者模式中,消費者主體就是一個死循環(huán)荧恍,它不停的從隊列中接受任務(wù)瓷叫,執(zhí)行任務(wù),在停止程序時送巡,我們需要一種”優(yōu)雅”的方法以關(guān)閉該線程
- 在一些場景中摹菠,比如從第三方服務(wù)器查詢一個結(jié)果,我們希望在限定的時間內(nèi)得到結(jié)果骗爆,如果得不到次氨,我們會希望取消該任務(wù)。
總之摘投,很多情況下我們都有關(guān)閉一個線程的需求煮寡,那么如何正確的關(guān)閉一個線程就是我們要研究的事情,這個事情在上一篇文章中已經(jīng)討論過了犀呼,這里不再贅述。
廢棄的API
Thread.STOP()之類的api會造成一些不可預知的bug外臂,所以很早便Deprecated了,真要糾結(jié)為什么請看這邊文章為何不贊成使用 Thread.stop、Thread.suspend 和 Thread.resume?
線程中斷API
Thread類定義了如下關(guān)于中斷的方法:
線程對中斷的反應
RUNNABLE:線程在運行或具備運行條件只是在等待操作系統(tǒng)調(diào)度
WAITING/TIMED_WAITING:線程在等待某個條件或超時
BLOCKED:線程在等待鎖,試圖進入同步塊
NEW/TERMINATED:線程還未啟動或已結(jié)束
RUNNABLE狀態(tài)
如果線程在運行中克握,interrupt()只是會設(shè)置線程的中斷標志位,沒有任何其它作用阔馋。線程應該在運行過程中合適的位置檢查中斷標志位玛荞,比如說,如果主體代碼是一個循環(huán)呕寝,可以在循環(huán)開始處進行檢查,如下所示:
public class InterruptRunnableDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// ... 單次循環(huán)代碼
}
System.out.println("done ");
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new InterruptRunnableDemo();
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
WAITING/TIMED_WAITING
線程執(zhí)行如下方法會進入WAITING狀態(tài):
public final void join() throws InterruptedException
public final void wait() throws InterruptedException
執(zhí)行如下方法會進入TIMED_WAITING狀態(tài):
public final native void wait(long timeout) throws InterruptedException;
public static native void sleep(long millis) throws InterruptedException;
public final synchronized void join(long millis) throws InterruptedException
在這些狀態(tài)時婴梧,對線程對象調(diào)用interrupt()會使得該線程拋出InterruptedException下梢,需要注意的是,拋出異常后塞蹭,中斷標志位會被清空(線程的中斷標志位會由true重置為false孽江,因為線程為了處理異常已經(jīng)重新處于就緒狀態(tài)。)番电,而不是被設(shè)置岗屏。比如說,執(zhí)行如下代碼:
Thread t = new Thread (){
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//exception被捕獲漱办,但是為輸出為false 因為標志位會被清空
System.out.println(isInterrupted());
}
}
};
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
t.interrupt();//置為true
InterruptedException是一個受檢異常这刷,線程必須進行處理。我們在異常處理中介紹過娩井,處理異常的基本思路是暇屋,如果你知道怎么處理,就進行處理洞辣,如果不知道咐刨,就應該向上傳遞,通常情況下扬霜,你不應該做的是定鸟,捕獲異常然后忽略。
捕獲到InterruptedException著瓶,通常表示希望結(jié)束該線程联予,線程大概有兩種處理方式:
1、向上傳遞該異常蟹但,這使得該方法也變成了一個可中斷的方法躯泰,需要調(diào)用者進行處理
2、有些情況华糖,不能向上傳遞異常麦向,比如Thread的run方法,它的聲明是固定的客叉,不能拋出任何受檢異常诵竭,這時话告,應該捕獲異常,進行合適的清理操作卵慰,清理后沙郭,一般應該調(diào)用Thread的interrupt方法設(shè)置中斷標志位,使得其他代碼有辦法知道它發(fā)生了中斷
第一種方式的示例代碼如下:
//拋出中斷異常裳朋,由調(diào)用者捕獲
public void interruptibleMethod() throws InterruptedException{
// ... 包含wait, join 或 sleep 方法
Thread.sleep(1000);
}
第二種方式的示例代碼如下:
public class InterruptWaitingDemo extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模擬任務(wù)代碼
Thread.sleep(2000);
} catch (InterruptedException e) {
// ... 清理操作
System.out.println(isInterrupted());//false
// 重設(shè)中斷標志位為true
Thread.currentThread().interrupt();
}
}
System.out.println(isInterrupted());//true
}
public static void main(String[] args) {
InterruptWaitingDemo thread = new InterruptWaitingDemo();
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
BLOCKED
如果線程在等待鎖病线,對線程對象調(diào)用interrupt()只是會設(shè)置線程的中斷標志位,線程依然會處于BLOCKED狀態(tài)鲤嫡,也就是說送挑,interrupt()并不能使一個在等待鎖的線程真正”中斷”。我們看段代碼:
public class InterruptSynchronizedDemo {
private static Object lock = new Object();//monitor
private static class A extends Thread {
@Override
public void run() {
//等待lock鎖
synchronized (lock) {
//等待標志位被置為true
while (!Thread.currentThread().isInterrupted()) {
}
}
System.out.println("exit");
}
}
public static void test() throws InterruptedException {
synchronized (lock) {//獲取鎖
A a = new A();
a.start();
Thread.sleep(1000);
//a在等待lock鎖暖眼,interrupt 無法中斷
a.interrupt();
//a線程加入當前線程惕耕,等待執(zhí)行完畢
a.join();
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
test方法在持有鎖lock的情況下啟動線程a,而線程a也去嘗試獲得鎖lock诫肠,所以會進入鎖等待隊列司澎,隨后test調(diào)用線程a的interrupt方法并等待線程線程a結(jié)束,線程a會結(jié)束嗎栋豫?不會挤安,interrupt方法只會設(shè)置線程的中斷標志,而并不會使它從鎖等待隊列中出來笼才。
我們稍微修改下代碼漱受,去掉test方法中的最后一行a.join,即變?yōu)椋?/p>
public static void test() throws InterruptedException {
synchronized (lock) {
A a = new A();
a.start();
Thread.sleep(1000);
a.interrupt();
}
//lock鎖釋放后 A線程重隊列中出來
}
這時骡送,程序就會退出昂羡。為什么呢?因為主線程不再等待線程a結(jié)束摔踱,釋放鎖lock后虐先,線程a會獲得鎖,然后檢測到發(fā)生了中斷派敷,所以會退出蛹批。
在使用synchronized關(guān)鍵字獲取鎖的過程中不響應中斷請求,這是synchronized的局限性篮愉。如果這對程序是一個問題腐芍,應該使用顯式鎖,java中的Lock接口试躏,它支持以響應中斷的方式獲取鎖猪勇。對于Lock.lock(),可以改用Lock.lockInterruptibly()颠蕴,可被中斷的加鎖操作泣刹,它可以拋出中斷異常助析。等同于等待時間無限長的Lock.tryLock(long time, TimeUnit unit)。
NEW/TERMINATE
如果線程尚未啟動(NEW)椅您,或者已經(jīng)結(jié)束(TERMINATED)外冀,則調(diào)用interrupt()對它沒有任何效果,中斷標志位也不會被設(shè)置掀泳。比如說雪隧,以下代碼的輸出都是false。
public class InterruptNotAliveDemo {
private static class A extends Thread {
@Override
public void run() {
}
}
public static void test() throws InterruptedException {
A a = new A();
a.interrupt();
System.out.println(a.isInterrupted());
a.start();
Thread.sleep(100);
a.interrupt();
System.out.println(a.isInterrupted());
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
IO操作
如果線程在等待IO操作开伏,尤其是網(wǎng)絡(luò)IO膀跌,則會有一些特殊的處理,我們沒有介紹過網(wǎng)絡(luò)固灵,這里只是簡單介紹下。
1劫流、實現(xiàn)此InterruptibleChannel接口的通道是可中斷的:如果某個線程在可中斷通道上因調(diào)用某個阻塞的 I/O 操作(常見的操作一般有這些:serverSocketChannel. accept()巫玻、socketChannel.connect、socketChannel.open祠汇、socketChannel.read仍秤、socketChannel.write、fileChannel.read可很、fileChannel.write)而進入阻塞狀態(tài)诗力,而另一個線程又調(diào)用了該阻塞線程的 interrupt 方法,這將導致該通道被關(guān)閉我抠,并且已阻塞線程接將會收到ClosedByInterruptException苇本,并且設(shè)置已阻塞線程的中斷狀態(tài)。另外菜拓,如果已設(shè)置某個線程的中斷狀態(tài)并且它在通道上調(diào)用某個阻塞的 I/O 操作瓣窄,則該通道將關(guān)閉并且該線程立即接收到 ClosedByInterruptException;并仍然設(shè)置其中斷狀態(tài)纳鼎。
2俺夕、如果線程阻塞于Selector調(diào)用,則線程的中斷標志位會被設(shè)置贱鄙,同時劝贸,阻塞的調(diào)用會立即返回。
我們重點介紹另一種情況逗宁,InputStream的read調(diào)用映九,該操作是不可中斷的,如果流中沒有數(shù)據(jù)疙剑,read會阻塞 (但線程狀態(tài)依然是RUNNABLE)氯迂,且不響應interrupt()践叠,與synchronized類似,調(diào)用interrupt()只會設(shè)置線程的中斷標志嚼蚀,而不會真正”中斷”它禁灼,我們看段代碼
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
System.out.println(System.in.read())//wait input
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.interrupt();
}
}
線程t啟動后調(diào)用System.in.read()從標準輸入讀入一個字符,不要輸入任何字符轿曙,我們會看到弄捕,調(diào)用interrupt()不會中斷read(),線程會一直運行导帝。
不過守谓,有一個辦法可以中斷read()調(diào)用,那就是調(diào)用流的close方法您单,我們將代碼改為:
public class InterruptReadDemo {
private static class A extends Thread {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(System.in.read());
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("exit");
}
public void cancel() {
try {
System.in.close();
} catch (IOException e) {
}
interrupt();
}
}
public static void main(String[] args) throws InterruptedException {
A t = new A();
t.start();
Thread.sleep(100);
t.cancel();
}
}
我們給線程定義了一個cancel方法斋荞,在該方法中,調(diào)用了流的close方法虐秦,同時調(diào)用了interrupt方法平酿,這次,程序會輸出:
-1
exit
也就是說悦陋,調(diào)用close方法后蜈彼,read方法會返回,返回值為-1俺驶,表示流結(jié)束幸逆。
如何正確地取消/關(guān)閉線程
以上,我們可以看出暮现,interrupt方法不一定會真正”中斷”線程还绘,它只是一種協(xié)作機制,如果 不明白線程在做什么送矩,不應該貿(mào)然的調(diào)用線程的interrupt方法蚕甥,以為這樣就能取消線程。
對于以線程提供服務(wù)的程序模塊而言栋荸,它應該封裝取消/關(guān)閉操作菇怀,提供單獨的取消/關(guān)閉方法給調(diào)用者,類似于InterruptReadDemo中演示的cancel方法晌块,外部調(diào)用者應該調(diào)用這些方法而不是直接調(diào)用interrupt爱沟。
Java并發(fā)庫的一些代碼就提供了單獨的取消/關(guān)閉方法,比如說匆背,F(xiàn)uture接口提供了如下方法以取消任務(wù):boolean cancel(boolean mayInterruptIfRunning);
再比如呼伸,ExecutorService提供了如下兩個關(guān)閉方法:
void shutdown();
List<Runnable> shutdownNow();
- Future和ExecutorService的API文檔對這些方法都進行了詳細說明,這是我們應該學習的方式。
原文地址:線程的中斷(interrupt)機制