Thread:怎么中斷一個(gè)線程砰粹?如何保證中斷業(yè)務(wù)不影響?

這樣的情景您也許并不陌生:您在編寫一個(gè)測(cè)試程序造挽,程序需要暫停一段時(shí)間碱璃,于是調(diào)用 Thread.sleep()。但是編譯器或 IDE 報(bào)錯(cuò)說(shuō)沒有處理檢查到的 InterruptedException饭入。InterruptedException 是什么呢嵌器,為什么必須處理它?

對(duì)于 InterruptedException谐丢,一種常見的處理方式是 “生吞(swallow)” 它 —— 捕捉它爽航,然后什么也不做(或者記錄下它,不過(guò)這也好不到哪去)—— 就像后面的 清單 4 一樣乾忱。不幸的是讥珍,這種方法忽略了這樣一個(gè)事實(shí):這期間可能發(fā)生中斷,而中斷可能導(dǎo)致應(yīng)用程序喪失及時(shí)取消活動(dòng)或關(guān)閉的能力窄瘟。

阻塞方法

當(dāng)一個(gè)方法拋出 InterruptedException 時(shí)衷佃,它不僅告訴您它可以拋出一個(gè)特定的檢查異常,而且還告訴您其他一些事情蹄葱。例如氏义,它告訴您它是一個(gè)阻塞(blocking)方法,如果您響應(yīng)得當(dāng)?shù)脑捦荚疲鼘L試消除阻塞并盡早返回觅赊。

阻塞方法不同于一般的要運(yùn)行較長(zhǎng)時(shí)間的方法。一般方法的完成只取決于它所要做的事情琼稻,以及是否有足夠多可用的計(jì)算資源(CPU 周期和內(nèi)存)。而阻塞方法的完成還取決于一些外部的事件饶囚,例如計(jì)時(shí)器到期帕翻,I/O 完成,或者另一個(gè)線程的動(dòng)作(釋放一個(gè)鎖萝风,設(shè)置一個(gè)標(biāo)志嘀掸,或者將一個(gè)任務(wù)放在一個(gè)工作隊(duì)列中)。一般方法在它們的工作做完后即可結(jié)束规惰,而阻塞方法較難于預(yù)測(cè)睬塌,因?yàn)樗鼈內(nèi)Q于外部事件。阻塞方法可能影響響應(yīng)能力,因?yàn)殡y于預(yù)測(cè)它們何時(shí)會(huì)結(jié)束揩晴。

阻塞方法可能因?yàn)榈炔坏剿鹊氖录鵁o(wú)法終止勋陪,因此令阻塞方法可取消 就非常有用(如果長(zhǎng)時(shí)間運(yùn)行的非阻塞方法是可取消的,那么通常也非常有用)硫兰∽缬蓿可取消操作是指能從外部使之在正常完成之前終止的操作。由 Thread 提供并受 Thread.sleep()Object.wait() 支持的中斷機(jī)制就是一種取消機(jī)制劫映;它允許一個(gè)線程請(qǐng)求另一個(gè)線程停止它正在做的事情违孝。當(dāng)一個(gè)方法拋出 InterruptedException 時(shí),它是在告訴您泳赋,如果執(zhí)行該方法的線程被中斷雌桑,它將嘗試停止它正在做的事情而提前返回,并通過(guò)拋出 InterruptedException 表明它提前返回祖今。 行為良好的阻塞庫(kù)方法應(yīng)該能對(duì)中斷作出響應(yīng)并拋出 InterruptedException校坑,以便能夠用于可取消活動(dòng)中,而不至于影響響應(yīng)衅鹿。

線程中斷

每個(gè)線程都有一個(gè)與之相關(guān)聯(lián)的 Boolean 屬性撒踪,用于表示線程的中斷狀態(tài)(interrupted status)。中斷狀態(tài)初始時(shí)為 false大渤;當(dāng)另一個(gè)線程通過(guò)調(diào)用 Thread.interrupt() 中斷一個(gè)線程時(shí)制妄,會(huì)出現(xiàn)以下兩種情況之一。如果那個(gè)線程在執(zhí)行一個(gè)低級(jí)可中斷阻塞方法泵三,例如 Thread.sleep()耕捞、 Thread.join()Object.wait(),那么它將取消阻塞并拋出 InterruptedException烫幕。否則俺抽, interrupt() 只是設(shè)置線程的中斷狀態(tài)。 在被中斷線程中運(yùn)行的代碼以后可以輪詢中斷狀態(tài)较曼,看看它是否被請(qǐng)求停止正在做的事情磷斧。中斷狀態(tài)可以通過(guò) Thread.isInterrupted() 來(lái)讀取,并且可以通過(guò)一個(gè)名為 Thread.interrupted() 的操作讀取和清除捷犹。

中斷是一種協(xié)作機(jī)制弛饭。當(dāng)一個(gè)線程中斷另一個(gè)線程時(shí),被中斷的線程不一定要立即停止正在做的事情萍歉。相反侣颂,中斷是禮貌地請(qǐng)求另一個(gè)線程在它愿意并且方便的時(shí)候停止它正在做的事情。有些方法枪孩,例如 Thread.sleep()憔晒,很認(rèn)真地對(duì)待這樣的請(qǐng)求藻肄,但每個(gè)方法不是一定要對(duì)中斷作出響應(yīng)。對(duì)于中斷請(qǐng)求拒担,不阻塞但是仍然要花較長(zhǎng)時(shí)間執(zhí)行的方法可以輪詢中斷狀態(tài)嘹屯,并在被中斷的時(shí)候提前返回。 您可以隨意忽略中斷請(qǐng)求澎蛛,但是這樣做的話會(huì)影響響應(yīng)抚垄。

中斷的協(xié)作特性所帶來(lái)的一個(gè)好處是,它為安全地構(gòu)造可取消活動(dòng)提供更大的靈活性谋逻。我們很少希望一個(gè)活動(dòng)立即停止呆馁;如果活動(dòng)在正在進(jìn)行更新的時(shí)候被取消,那么程序數(shù)據(jù)結(jié)構(gòu)可能處于不一致狀態(tài)毁兆。中斷允許一個(gè)可取消活動(dòng)來(lái)清理正在進(jìn)行的工作浙滤,恢復(fù)不變量,通知其他活動(dòng)它要被取消气堕,然后才終止纺腊。

處理 InterruptedException

如果拋出 InterruptedException 意味著一個(gè)方法是阻塞方法,那么調(diào)用一個(gè)阻塞方法則意味著您的方法也是一個(gè)阻塞方法茎芭,而且您應(yīng)該有某種策略來(lái)處理 InterruptedException揖膜。通常最容易的策略是自己拋出 InterruptedException,如清單 1 中 putTask()getTask() 方法中的代碼所示梅桩。 這樣做可以使方法對(duì)中斷作出響應(yīng)壹粟,并且只需將 InterruptedException 添加到 throws 子句。

清單 1. 不捕捉 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();

}

}

有時(shí)候需要在傳播異常之前進(jìn)行一些清理工作趁仙。在這種情況下,可以捕捉 InterruptedException垦页,執(zhí)行清理雀费,然后拋出異常。清單 2 演示了這種技術(shù)痊焊,該代碼是用于匹配在線游戲服務(wù)中的玩家的一種機(jī)制盏袄。 matchPlayers() 方法等待兩個(gè)玩家到來(lái),然后開始一個(gè)新游戲薄啥。如果在一個(gè)玩家已到來(lái)貌矿,但是另一個(gè)玩家仍未到來(lái)之際該方法被中斷,那么它會(huì)將那個(gè)玩家放回隊(duì)列中罪佳,然后重新拋出 InterruptedException,這樣那個(gè)玩家對(duì)游戲的請(qǐng)求就不至于丟失黑低。

清單 2. 在重新拋出 InterruptedException 之前執(zhí)行特定于任務(wù)的清理工作

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;

}

}

}

不要生吞中斷

有時(shí)候拋出 InterruptedException 并不合適赘艳,例如當(dāng)由 Runnable 定義的任務(wù)調(diào)用一個(gè)可中斷的方法時(shí)酌毡,就是如此。在這種情況下蕾管,不能重新拋出 InterruptedException枷踏,但是您也不想什么都不做。當(dāng)一個(gè)阻塞方法檢測(cè)到中斷并拋出 InterruptedException 時(shí)掰曾,它清除中斷狀態(tài)旭蠕。如果捕捉到 InterruptedException 但是不能重新拋出它,那么應(yīng)該保留中斷發(fā)生的證據(jù)旷坦,以便調(diào)用棧中更高層的代碼能知道中斷掏熬,并對(duì)中斷作出響應(yīng)。該任務(wù)可以通過(guò)調(diào)用 interrupt() 以 “重新中斷” 當(dāng)前線程來(lái)完成秒梅,如清單 3 所示旗芬。至少,每當(dāng)捕捉到 InterruptedException 并且不重新拋出它時(shí)捆蜀,就在返回之前重新中斷當(dāng)前線程疮丛。

清單 3. 捕捉 InterruptedException 后恢復(fù)中斷狀態(tài)

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();

}

}

}

處理 InterruptedException 時(shí)采取的最糟糕的做法是生吞它 —— 捕捉它,然后既不重新拋出它辆它,也不重新斷言線程的中斷狀態(tài)誊薄。對(duì)于不知如何處理的異常,最標(biāo)準(zhǔn)的處理方法是捕捉它锰茉,然后記錄下它呢蔫,但是這種方法仍然無(wú)異于生吞中斷,因?yàn)檎{(diào)用棧中更高層的代碼還是無(wú)法獲得關(guān)于該異常的信息洞辣。(僅僅記錄 InterruptedException 也不是明智的做法咐刨,因?yàn)榈鹊饺藖?lái)讀取日志的時(shí)候,再來(lái)對(duì)它作出處理就為時(shí)已晚了扬霜。) 清單 4 展示了一種使用得很廣泛的模式定鸟,這也是生吞中斷的一種模式:

清單 4. 生吞中斷 —— 不要這么做

// Don't do this

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 swallowed) {

/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */

}

}

}

如果不能重新拋出 InterruptedException,不管您是否計(jì)劃處理中斷請(qǐng)求著瓶,仍然需要重新中斷當(dāng)前線程联予,因?yàn)橐粋€(gè)中斷請(qǐng)求可能有多個(gè) “接收者”。標(biāo)準(zhǔn)線程池 (ThreadPoolExecutor)worker 線程實(shí)現(xiàn)負(fù)責(zé)中斷材原,因此中斷一個(gè)運(yùn)行在線程池中的任務(wù)可以起到雙重效果沸久,一是取消任務(wù),二是通知執(zhí)行線程線程池正要關(guān)閉余蟹。如果任務(wù)生吞中斷請(qǐng)求卷胯,則 worker 線程將不知道有一個(gè)被請(qǐng)求的中斷,從而耽誤應(yīng)用程序或服務(wù)的關(guān)閉威酒。

實(shí)現(xiàn)可取消任務(wù)

語(yǔ)言規(guī)范中并沒有為中斷提供特定的語(yǔ)義窑睁,但是在較大的程序中挺峡,難于維護(hù)除取消外的任何中斷語(yǔ)義。取決于是什么活動(dòng)担钮,用戶可以通過(guò)一個(gè) GUI 或通過(guò)網(wǎng)絡(luò)機(jī)制橱赠,例如 JMX 或 Web 服務(wù)來(lái)請(qǐng)求取消。程序邏輯也可以請(qǐng)求取消箫津。例如狭姨,一個(gè) Web 爬行器(crawler)如果檢測(cè)到磁盤已滿,它會(huì)自動(dòng)關(guān)閉自己苏遥,否則一個(gè)并行算法會(huì)啟動(dòng)多個(gè)線程來(lái)搜索解決方案空間的不同區(qū)域饼拍,一旦其中一個(gè)線程找到一個(gè)解決方案,就取消那些線程暖眼。

僅僅因?yàn)橐粋€(gè)任務(wù)是可取消的惕耕,并不意味著需要立即 對(duì)中斷請(qǐng)求作出響應(yīng)。對(duì)于執(zhí)行一個(gè)循環(huán)中的代碼的任務(wù)诫肠,通常只需為每一個(gè)循環(huán)迭代檢查一次中斷司澎。取決于循環(huán)執(zhí)行的時(shí)間有多長(zhǎng),任何代碼可能要花一些時(shí)間才能注意到線程已經(jīng)被中斷(或者是通過(guò)調(diào)用 Thread.isInterrupted() 方法輪詢中斷狀態(tài)栋豫,或者是調(diào)用一個(gè)阻塞方法)挤安。 如果任務(wù)需要提高響應(yīng)能力,那么它可以更頻繁地輪詢中斷狀態(tài)丧鸯。阻塞方法通常在入口就立即輪詢中斷狀態(tài)蛤铜,并且,如果它被設(shè)置來(lái)改善響應(yīng)能力丛肢,那么還會(huì)拋出 InterruptedException围肥。

惟一可以生吞中斷的時(shí)候是您知道線程正要退出。只有當(dāng)調(diào)用可中斷方法的類是 Thread 的一部分蜂怎,而不是 Runnable 或通用庫(kù)代碼的情況下穆刻,才會(huì)發(fā)生這樣的場(chǎng)景,清單 5 演示了這種情況杠步。清單 5 創(chuàng)建一個(gè)線程氢伟,該線程列舉素?cái)?shù),直到被中斷幽歼,這里還允許該線程在被中斷時(shí)退出朵锣。用于搜索素?cái)?shù)的循環(huán)在兩個(gè)地方檢查是否有中斷:一處是在 while 循環(huán)的頭部輪詢 isInterrupted() 方法,另一處是調(diào)用阻塞方法 BlockingQueue.put()甸私。

清單 5. 如果知道線程正要退出的話诚些,則可以生吞中斷

public class PrimeProducer extends Thread {

private final BlockingQueue<``BigInteger``> queue;

PrimeProducer(BlockingQueue<``BigInteger``> queue) {

this.queue = queue;

}

public void run() {

try {

BigInteger p = BigInteger.ONE;

while (!Thread.currentThread().isInterrupted())

queue.put(p = p.nextProbablePrime());

} catch (InterruptedException consumed) {

/* Allow thread to exit */

}

}

public void cancel() { interrupt(); }

}

|

不可中斷的阻塞方法

并非所有的阻塞方法都拋出 InterruptedException。輸入和輸出流類會(huì)阻塞等待 I/O 完成皇型,但是它們不拋出 InterruptedException泣刹,而且在被中斷的情況下也不會(huì)提前返回助析。然而,對(duì)于套接字 I/O椅您,如果一個(gè)線程關(guān)閉套接字,則那個(gè)套接字上的阻塞 I/O 操作將提前結(jié)束寡键,并拋出一個(gè) SocketException掀泳。java.nio 中的非阻塞 I/O 類也不支持可中斷 I/O,但是同樣可以通過(guò)關(guān)閉通道或者請(qǐng)求 Selector 上的喚醒來(lái)取消阻塞操作西轩。類似地员舵,嘗試獲取一個(gè)內(nèi)部鎖的操作(進(jìn)入一個(gè) synchronized 塊)是不能被中斷的,但是 ReentrantLock 支持可中斷的獲取模式藕畔。

不可取消的任務(wù)

有些任務(wù)拒絕被中斷马僻,這使得它們是不可取消的。但是注服,即使是不可取消的任務(wù)也應(yīng)該嘗試保留中斷狀態(tài)韭邓,以防在不可取消的任務(wù)結(jié)束之后,調(diào)用棧上更高層的代碼需要對(duì)中斷進(jìn)行處理溶弟。清單 6 展示了一個(gè)方法女淑,該方法等待一個(gè)阻塞隊(duì)列,直到隊(duì)列中出現(xiàn)一個(gè)可用項(xiàng)目辜御,而不管它是否被中斷鸭你。為了方便他人,它在結(jié)束后在一個(gè) finally 塊中恢復(fù)中斷狀態(tài)擒权,以免剝奪中斷請(qǐng)求的調(diào)用者的權(quán)利袱巨。(它不能在更早的時(shí)候恢復(fù)中斷狀態(tài),因?yàn)槟菍?dǎo)致無(wú)限循環(huán) —— BlockingQueue.take() 將在入口處立即輪詢中斷狀態(tài)碳抄,并且愉老,如果發(fā)現(xiàn)中斷狀態(tài)集,就會(huì)拋出 InterruptedException纳鼎。)

清單 6. 在返回前恢復(fù)中斷狀態(tài)的不可取消任務(wù)

public Task getNextTask(BlockingQueue<``Task``> queue) {

boolean interrupted = false;

try {

while (true) {

try {

return queue.take();

} catch (InterruptedException e) {

interrupted = true;

// fall through and retry

}

}

} finally {

if (interrupted)

Thread.currentThread().interrupt();

}

}

結(jié)束語(yǔ)

您可以用 Java 平臺(tái)提供的協(xié)作中斷機(jī)制來(lái)構(gòu)造靈活的取消策略俺夕。各活動(dòng)可以自行決定它們是可取消的還是不可取消的,以及如何對(duì)中斷作出響應(yīng)贱鄙,如果立即返回會(huì)危害應(yīng)用程序完整性的話劝贸,它們還可以推遲中斷。即使您想在代碼中完全忽略中斷逗宁,也應(yīng)該確保在捕捉到 InterruptedException 但是沒有重新拋出它的情況下映九,恢復(fù)中斷狀態(tài),以免調(diào)用它的代碼無(wú)法獲知中斷的發(fā)生瞎颗。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末件甥,一起剝皮案震驚了整個(gè)濱河市捌议,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌引有,老刑警劉巖瓣颅,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異譬正,居然都是意外死亡宫补,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門曾我,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)粉怕,“玉大人,你說(shuō)我怎么就攤上這事抒巢∑侗矗” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵蛉谜,是天一觀的道長(zhǎng)稚晚。 經(jīng)常有香客問(wèn)我,道長(zhǎng)悦陋,這世上最難降的妖魔是什么蜈彼? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮俺驶,結(jié)果婚禮上幸逆,老公的妹妹穿的比我還像新娘。我一直安慰自己暮现,他們只是感情好还绘,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著栖袋,像睡著了一般拍顷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塘幅,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天昔案,我揣著相機(jī)與錄音,去河邊找鬼电媳。 笑死踏揣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的匾乓。 我是一名探鬼主播捞稿,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了娱局?” 一聲冷哼從身側(cè)響起彰亥,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎衰齐,沒想到半個(gè)月后任斋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耻涛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年仁卷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犬第。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖芒帕,靈堂內(nèi)的尸體忽然破棺而出歉嗓,到底是詐尸還是另有隱情,我是刑警寧澤背蟆,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布鉴分,位于F島的核電站,受9級(jí)特大地震影響带膀,放射性物質(zhì)發(fā)生泄漏志珍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一垛叨、第九天 我趴在偏房一處隱蔽的房頂上張望伦糯。 院中可真熱鬧,春花似錦嗽元、人聲如沸敛纲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)淤翔。三九已至,卻和暖如春佩谷,著一層夾襖步出監(jiān)牢的瞬間旁壮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工谐檀, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抡谐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓稚补,卻偏偏與公主長(zhǎng)得像童叠,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361