一玄呛、前言
所謂線程中斷诬留,其實(shí)就是終止一個(gè)線程。在使用 Java 線程時(shí)搔弄,除了線程自行正常結(jié)束幅虑,很多時(shí)候也需要提前結(jié)束一個(gè)線程的執(zhí)行過(guò)程。Thread 類(lèi)中有一個(gè)與 start() 相對(duì)應(yīng)的 stop() 方法顾犹,可以從外部結(jié)束一個(gè)線程的執(zhí)行倒庵。但是這個(gè)方法是極不推薦使用的,因?yàn)閺耐獠繌?qiáng)行結(jié)束一個(gè)線程的執(zhí)行炫刷,會(huì)導(dǎo)致不可預(yù)知的錯(cuò)誤擎宝,因?yàn)檫@樣往往會(huì)在錯(cuò)誤的時(shí)間結(jié)束一個(gè)線程的執(zhí)行。
所以浑玛,在 Java 線程機(jī)制中绍申,就有了另一種結(jié)束線程的方式,那就是中斷。中斷极阅,簡(jiǎn)而言之就是讓線程外部可以設(shè)置一個(gè)標(biāo)記值胃碾,而線程內(nèi)部在執(zhí)行時(shí)則檢查這個(gè)值,來(lái)獲知此線程是否應(yīng)該結(jié)束了涂屁。
二书在、可以用來(lái)設(shè)置中斷的方法
除了 Thread.interrupt() 方法以外,下列 JDK 中的方法也會(huì)設(shè)置中斷(也是通過(guò)調(diào)用 Thread.interrupt()
來(lái)實(shí)現(xiàn)的):
FutureTask.cancel()
-
ExecutorService.shutdownNow()
這個(gè)方法會(huì)調(diào)用線程池中所有線程的中斷方法拆又,不論它們是空閑的還是運(yùn)行中的儒旬。而ExecutorService.shutdown()
方法只能中斷空閑的線程。
上面只是舉兩個(gè) JDK 中應(yīng)用到了線程中斷的例子帖族,這樣的例子還有很多栈源,就不一一列舉了。當(dāng)然竖般,為了能響應(yīng)中斷甚垦,在你所寫(xiě)的 Runnable 或 Callable 代碼中,必須通過(guò) Thread.isInterrupted()
涣雕、Thread.interrupted()
方法艰亮,或者捕獲 InterruptedException
等的中斷異常來(lái)發(fā)現(xiàn)線程中斷并處理,否則線程是不會(huì)自行提前結(jié)束的挣郭。
三迄埃、能被中斷的方法
在 JDK 和其它類(lèi)庫(kù)和框架中,能相應(yīng)中斷的方法是很多的兑障。下面列出幾個(gè)常見(jiàn)的 JDK 中能響應(yīng)中斷的方法:
- Thread.sleep()
- Object.wait()
- BlockingQueue.put(), BlockingQueue.take()
- ReentrantLock.lockInterruptibly(), Condition.await()
- ServerSocketChannel.accept(), SocketChannel.open()
等等
JDK 中能響應(yīng)中斷的方法基本上都是拋出異常侄非。這些方法基本可被分為兩類(lèi):一類(lèi)是并發(fā)相關(guān)的,一類(lèi)是 IO 相關(guān)的流译。
四逞怨、中斷的處理
1. 處理 InterruptedException(也包括其它中斷異常)
InterruptedException
是最常見(jiàn)的中斷表現(xiàn)形式。所以如何處理 InterruptedException
便成為 Java 中斷知識(shí)中的必修課福澡。在這方面 IBM developerWorks 上有篇文章講的很好叠赦,我在下面的參考文章中會(huì)列出鏈接。我這里就對(duì)這篇文章做一個(gè)總結(jié)革砸,各位看客可以去讀那邊文章以獲得細(xì)節(jié)知識(shí)眯搭。
處理 InterruptedException 可有以下幾種方式(下面使用的代碼均引用自 Java 理論與實(shí)踐: 處理 InterruptedException):
直接向上拋出
將異常不做任何處理,直接拋向該方法的調(diào)用者
public class TaskQueue {
private static final int MAX_TASKS = 1000;
private BlockingQueue<Task> queue
= new LinkedBlockingQueue<Task>(MAX_TASKS);
public void putTask(Task r) throws InterruptedException {
queue.put(r);
}
public Task getTask() throws InterruptedException {
return queue.take();
}
}
在 catch 中做處理后在拋出
因?yàn)?InterruptedException
的拋出业岁,會(huì)打斷方法執(zhí)行鳞仙,使正在進(jìn)行的工作只完成一部分。在有些情況下笔时,你就需要進(jìn)行諸如回滾的處理棍好。所以在這種情況便需要在 catch 塊中進(jìn)行處理之后在向上拋出 InterruptedException
。
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
try {
Player playerOne, playerTwo;
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
不拋出 InterruptedException 時(shí)要恢復(fù)中斷狀態(tài)
很多時(shí)候,由于你所實(shí)現(xiàn)的接口定義的限制借笙,你很可能無(wú)法拋出 InterruptedException
扒怖。例如實(shí)現(xiàn) Runnable
接口以編寫(xiě)業(yè)務(wù)代碼。這時(shí)业稼,你就無(wú)法再向上拋出 InterruptedException
了盗痒。此時(shí)你應(yīng)該使用 Thread.currentThread().interrupt()
方法去恢復(fù)中斷狀態(tài)。因?yàn)樽枞椒ㄔ趻伋?InterruptedException
時(shí)會(huì)清除當(dāng)前線程的中斷狀態(tài)低散,如果此時(shí)不恢復(fù)中斷狀態(tài)俯邓,也不拋出 InterruptedException
,那中斷信息便會(huì)丟失熔号,上層調(diào)用者也就無(wú)法得知中斷的發(fā)生稽鞭。這樣便有可能導(dǎo)致任務(wù)無(wú)法正確終止的情況方式。
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
在這里引镊,我們要清楚中斷的意義在于并發(fā)或異步場(chǎng)景下任務(wù)的終止朦蕴。所以,如果你的代碼在吞掉 InterruptedException
而不拋出時(shí)并不會(huì)造成任務(wù)無(wú)法被正確終止的情況方式弟头,那也可以不再恢復(fù)中斷吩抓。
還是 Runnable 的例子,大家都知道 Runnable
多數(shù)時(shí)候是提交到線程池來(lái)運(yùn)行赴恨。并且通常是作為任務(wù)的頂層容器來(lái)使用的疹娶,也就是說(shuō)在線程池和 Runnable
實(shí)現(xiàn)之間,沒(méi)有別的調(diào)用層了嘱支。那么在 try-catch InterruptedException
之后,便可不用在恢復(fù)線程中斷了挣饥。
但如果不是上述情況除师,你所寫(xiě)的,帶有 try-catch InterruptedException
的方法會(huì)被其它的扔枫、非線程池類(lèi)的方法調(diào)用汛聚。例如有 A, B 兩個(gè)方法,A 被 B 方法調(diào)用短荐,A 中捕獲 InterruptedException
后沒(méi)有恢復(fù)線程中斷倚舀,而 B 方法中有一個(gè)循環(huán),通過(guò)檢查線程中斷來(lái)決定是否退出忍宋,或者 B 方法在調(diào)用 A 方法之后痕貌,還有個(gè)阻塞的方法。如果不恢復(fù)線程中斷糠排,那便會(huì)造成線程無(wú)法按照期望被終止的情況發(fā)生舵稠。
自己拋出 InterruptedException
有時(shí)候,你需要“無(wú)中生有”地創(chuàng)造出一個(gè) InterruptedException
以表示中斷的發(fā)生。在這個(gè)時(shí)候哺徊,你需要使用 Thread.isInterrupted()
或 Thread.interrupted()
來(lái)檢測(cè)中斷的發(fā)生室琢。那究竟是用這兩者中的哪一個(gè)?其實(shí)看了前面的部分我們知道落追,拋出 InterruptedException
時(shí)盈滴,線程中斷狀態(tài)便被清除。所以轿钠,在你自己實(shí)現(xiàn)類(lèi)似功能的時(shí)候巢钓,也要遵循這一原則,即拋出 InterruptedException
后需要清除當(dāng)前線程的中斷狀態(tài)谣膳。因此竿报,此時(shí)需要使用 Thread.interrupted()
。
其實(shí)继谚,你要是看 JDK 源代碼烈菌,就會(huì)發(fā)現(xiàn),JDK 中并發(fā)類(lèi)也是這么做的
NOTE: 使用 Thread.interrupt() 和 InterruptedException 中的哪種方法表示中斷花履?上面提到了一種情況是由于接口的限制而無(wú)法拋出 InterruptedException芽世,這時(shí)你別無(wú)選擇,只能用 Thread.interrupt() 恢復(fù)中斷诡壁。除了這種情況济瓢,其它的時(shí)候推薦使用 InterruptedException 來(lái)表示中斷。當(dāng)方法聲明拋出 InterruptedException 時(shí)妹卿,它就是在告訴調(diào)用者旺矾,我這個(gè)方法可能會(huì)花費(fèi)很多的時(shí)間,而你可以通過(guò)線程中斷來(lái)終止調(diào)用夺克。通過(guò) InterruptedException 來(lái)表示中斷箕宙,含義更清晰,反應(yīng)也更迅速铺纽。
2. 無(wú)法被中斷的情況
synchronized
阻塞在 synchronized 的內(nèi)置鎖上是無(wú)法被中斷的柬帕,如果需要可以被中斷的鎖〗泼牛可以使用 Java 5 concurrent 中的 Lock陷寝。
Java IO(不包含 NIO 和 AIO)
對(duì) Java IO 有了解的人都知道,ServerSocket.accept()
是一個(gè)阻塞方法其馏,但是 Thread.interrupt()
對(duì)它毫無(wú)影響凤跑。要想終止 ServerSocket.accept()
的等待,唯一方法就是調(diào)用 close() 方法叛复。
3. 線程中斷與 Java IO
上面說(shuō)到傳統(tǒng)的 Java IO 中的阻塞方法無(wú)法響應(yīng)線程中斷饶火,因?yàn)?Java IO 出現(xiàn)的時(shí)候還沒(méi)有中斷機(jī)制鹏控。在 Java 1.4 引入的 Java NIO 已經(jīng)可以很好地支持 Java 線程中斷了。
如果是使用 XXXChannel肤寝,線程中斷會(huì)導(dǎo)致 accept(), open() 等阻塞方法拋出 ClosedByInterruptException当辐。
五、線程中斷與微服務(wù)的高可用
談到微服務(wù)的高可用就不得不說(shuō)熔斷降級(jí)鲤看,談到熔斷降級(jí)就不得不說(shuō) Hystrix缘揪。而 Hystrix 超時(shí)取消任務(wù)其實(shí)就是使用的線程中斷。當(dāng)任務(wù)執(zhí)行超過(guò) HystrixCommand 中的超時(shí)設(shè)置時(shí)义桂,Hystrix 便會(huì)在中斷執(zhí)行任務(wù)的線程找筝。所以,當(dāng)你使用 Hystrix 時(shí)慷吊,你的代碼一定要能響應(yīng)中斷袖裕。