天天看點

InterruptedException 和 interrupting threads 的一些說明

讓我們舉一個簡單的例子,有一個線程周期性地進行清理工作,其他時間都處于休眠狀态:

這段代碼在很多層面都有錯誤!

1.在一些環境中,利用構造函數啟動thread可能并不是一個好主意,例如像spring這類的架構會創造動态子類去支援方法攔截,最終我們會得到從兩個不同執行個體運作産生的兩個線程。

2.interruptedexception被吞掉了(吞掉指的是捕捉到了異常,之後繼續程式執行,就像沒發生一樣),異常本身沒有被日志正确地記錄下來。

4.使用scheduledthreadpoolexecutor我們可以避免手動地編寫休眠/工作循環,而且可以切換到fixed-rate(任務執行的間隔周期以一定比率增長)而不是這裡的fixed-delay(任務執行間隔周期不變)。

5.最後很重要的是當cleaner的執行個體不再被引用時,沒有方法銷毀它創造的線程。

所有問題都是有效的,但吞掉interruptedexception是最大的問題。在我們搞清楚為什麼之前,讓我們思考一下這個異常意味着什麼,我們怎樣才能利用它優雅地打斷線程。在jdk中有些阻塞操作聲明會抛出interruptedexception:

object.wait()

thread.sleep()

process.waitfor()

asynchronouschannelgroup.awaittermination()

java.util.concurrent.*中各種阻塞的方法, 例如 executorservice.awaittermination(), future.get(), blockingqueue.take(), semaphore.acquire(), condition.await() 還有很多其他的

swingutilities.invokeandwait()

注意到阻塞的i/o操作不會抛出interruptedexception(這是個恥辱)。如果所有的類都聲明了interruptedexception,你可能想知道這個異常是什麼時候被抛出過?

當一個線程被一些聲明了interruptedexception的方法阻塞時,你對這個線程調用thread.interrupt(),那麼大多數這樣的方法會立即抛出interruptedexception.

如果你向一個線程池送出任務(executorservice.submit()),這個任務正在被執行的時候你調用了future.cancel(ture)

在這種情況下,線程池會試着為你打斷正在執行這個任務的線程,以便有效地打斷你的任務。

了解interruptedexception的真正含義,我們就具備了正确處理它的能力。如果有些人試着去打斷我們的線程,而我們通過捕捉interruptedexception發現了它,這時候最合理的事情就是結束上述被打斷的線程。

注意到try-catch塊把整個while循環給包起來了,在這種方法中如果sleep()抛出了interruptedexception,我們将退出循環。你可能會說應該将interruptedexception的堆棧跟蹤用日志記錄下來,這個要視情況而定,在本例中打斷一個線程是我們所期望看到的,不是因為失敗而産生的。而處理的方法取決于你,底線是如果sleep()被另一個線程打斷,我們應該快速地從整個run()中跳出來。如果你很細心的話你可能會問當線程運作到cleanup()而不是sleep()時被打斷,那将會發生什麼?你經常會遇到這樣的人工标志:

注意到stop标志(它必須被volatile修飾)不會打斷阻塞的操作,我們不得不等待直到sleep()結束。另一方面明确的标志如stop能讓我們在任何時刻監控它的值以便更好的控制結束。而且這種方法被證明和線程中斷的原理是一樣的。如果有些人當線程正執行非阻塞計算的時候(比如cleanup())試圖中斷線程,此時這種計算是不能立刻被打斷的。然而線程已經被标記為interrupted,線上程的後續操作中(比如sleep())将會立刻直接抛出interruptedexception.

如果我們寫了一個非阻塞的線程卻仍然想要利用線程中斷的便利,我們可以簡單周期性地去檢查thread.isinterrupted(),而不必依賴interruptedexception:

對于上面的代碼,如果有人想要中斷線程,那麼一旦someheavycomputations()傳回我們将立刻放棄計算。如果它的執行花費太長時間或者無限制的執行,我們将不會識别到中斷标志。有趣的是interrupted标志不是一次性的(一次性指的是改變這個标志的值之後,它就沒用了,要想繼續使用需要手動把它給變回原樣,如那個stop标志),我們能調用thread.interrupted()而不是isinterrupted(),這樣的話interrupted标志就會被重設( <code>thread.interrupted()會讀取并清除中斷标志</code>)我們就能夠繼續我們的工作了。偶爾你想要忽略中斷标志,保持程式的運作,在這樣的情況下interrupted()就變得非常友善。

注意thread.stop()

uninterruptibles from guava

罕見地,你可能想要完全忽略interruptedexception,對于這種情況你可以檢視guava的uninterruptibles。它有大量實用的方法像sleepuninterruptibly()和awaituninterruptibly(countdownlatch),要小心這些方法。我知道這些方法都沒有聲明interruptedexception(這個異常可能很棘手),但這些方法卻能夠完全讓目前的線程免于被中斷-這可是相當難得的。

總結

到這裡我想你已經有些明白為什麼某些方法會抛出interruptedexception(你們知道為什麼會抛出這個異常嗎?是因為某些方法在調用之後會一直阻塞線程,包括大量耗時的計算、讓線程等待或者睡眠,這些方法導緻線程會花很長時間卡在那或者永遠卡在那。這時你會怎麼辦呢?為了讓程式繼續運作,避免卡在這種地方,你需要中斷這種卡死的線程并抛出是哪裡導緻線程阻塞。是以某些方法就需要聲明interruptedexception,表明這個方法會響應線程中斷。而之前講過中斷線程可以用thread.interrupt()或者使用人工的中斷标志),最主要的幾點是:

捕獲interruptedexception之後應該适當地對它進行處理-大多數情況下适當的處理指的是完全地跳出目前任務/循環/線程。

吞掉interruptedexception不是一個好主意

如果線程在非阻塞調用中被打斷了,這時應該使用isinterrupted()。當線程早已被打斷(也就是被标記為interrupted)時,一旦進入阻塞方法就應該立刻抛出interruptedexception。