閱讀331 返回首頁    go 技術社區[雲棲]


InterruptedException 和 interrupting threads 的一些說明

如果InterruptedException沒有檢測到異常,可能沒人會注意到它,這會導致很多bug不被發現。而檢測到這個異常的人大多數都是草率地、不恰當地處理著它。

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

class Cleaner implements Runnable {

  Cleaner() {
    final Thread cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }

  @Override
  public void run() {
    while(true) {
      cleanUp();
      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }

  private void cleanUp() {
    //...
  }

}

這段代碼在很多層麵都有錯誤!
1.在一些環境中,利用構造函數啟動Thread可能並不是一個好主意,例如像Spring這類的框架會創造動態子類去支持方法攔截,最終我們會得到從兩個不同實例運行產生的兩個線程。
2.InterruptedException被吞掉了(吞掉指的是捕捉到了異常,之後繼續程序執行,就像沒發生一樣),異常本身沒有被日誌正確地記錄下來。
3.這個類為每一個實例開啟一個新線程,應該使用ScheduledThreadPoolExecutor來代替,由那些實例共同分享線程池(有更高的穩定性和內存效率)。
4.使用ScheduledThreadPoolExecutor我們可以避免手動地編寫休眠/工作循環,而且可以切換到fixed-rate(任務執行的間隔周期以一定比率增長)而不是這裏的fixed-delay(任務執行間隔周期不變)。
5.最後很重要的是當Cleaner的實例不再被引用時,沒有方法銷毀它創造的線程。
所有問題都是有效的,但吞掉InterruptedException是最大的問題。在我們搞清楚為什麼之前,讓我們思考一下這個異常意味著什麼,我們怎樣才能利用它優雅地打斷線程。在JDK中有些阻塞操作聲明會拋出InterruptedException:

  • Object.wait()
  • Thread.sleep()
  • Process.waitFor()
  • 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發現了它,這時候最合理的事情就是結束上述被打斷的線程。

class Cleaner implements Runnable, AutoCloseable {

  private final Thread cleanerThread;

  Cleaner() {
    cleanerThread = new Thread(this, "Cleaner");
    cleanerThread.start();
  }

  @Override
  public void run() {
    try {
      while (true) {
        cleanUp();
        TimeUnit.SECONDS.sleep(1);
      }
    } catch (InterruptedException ignored) {
      log.debug("Interrupted, closing");
    }
  }

  //...   

  @Override
  public void close() {
    cleanerThread.interrupt();
  }
}

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

private volatile boolean stop = false;

@Override
public void run() {
  while (!stop) {
    cleanUp();
    TimeUnit.SECONDS.sleep(1);
  }
}

@Override
public void close() {
  stop = true;
}

注意到stop標誌(它必須被volatile修飾)不會打斷阻塞的操作,我們不得不等待直到sleep()結束。另一方麵明確的標誌如stop能讓我們在任何時刻監控它的值以便更好的控製結束。而且這種方法被證明和線程中斷的原理是一樣的。如果有些人當線程正執行非阻塞計算的時候(比如cleanUp())試圖中斷線程,此時這種計算是不能立刻被打斷的。然而線程已經被標記為interrupted,在線程的後續操作中(比如sleep())將會立刻直接拋出InterruptedException.
如果我們寫了一個非阻塞的線程卻仍然想要利用線程中斷的便利,我們可以簡單周期性地去檢查Thread.isInterrupted(),而不必依賴InterruptedException:

public void run() {
  while (!Thread.currentThread().isInterrupted()) {
    someHeavyComputations();
  }
}

對於上麵的代碼,如果有人想要中斷線程,那麼一旦someHeavyComputations()返回我們將立刻放棄計算。如果它的執行花費太長時間或者無限製的執行,我們將不會識別到中斷標誌。有趣的是interrupted標誌不是一次性的(一次性指的是改變這個標誌的值之後,它就沒用了,要想繼續使用需要手動把它給變回原樣,如那個stop標誌),我們能調用Thread.interrupted()而不是isInterrupted(),這樣的話interrupted標誌就會被重設( Thread.interrupted()會讀取並清除中斷標誌)我們就能夠繼續我們的工作了。偶爾你想要忽略中斷標誌,保持程序的運行,在這樣的情況下interrupted()就變得非常方便。
注意Thread.stop()
如果你是資曆較老的程序員的話,你也許可以調用Thread.stop(),雖然它早已被棄用10年了。Java 8早已計劃去”de-implement it”,但在1.8u5中它仍然存在。盡管如此,不要使用它,也不要將Thread.stop()重構到任何代碼的Thread.interrupt()中去。
Uninterruptibles from Guava
罕見地,你可能想要完全忽略InterruptedException,對於這種情況你可以查看Guava的Uninterruptibles。它有大量實用的方法像sleepUninterruptibly()和awaitUninterruptibly(CountDownLatch),要小心這些方法。我知道這些方法都沒有聲明InterruptedException(這個異常可能很棘手),但這些方法卻能夠完全讓當前的線程免於被中斷-這可是相當難得的。
總結
到這裏我想你已經有些明白為什麼某些方法會拋出InterruptedException(你們知道為什麼會拋出這個異常嗎?是因為某些方法在調用之後會一直阻塞線程,包括大量耗時的計算、讓線程等待或者睡眠,這些方法導致線程會花很長時間卡在那或者永遠卡在那。這時你會怎麼辦呢?為了讓程序繼續運行,避免卡在這種地方,你需要中斷這種卡死的線程並拋出是哪裏導致線程阻塞。所以某些方法就需要聲明InterruptedException,表明這個方法會響應線程中斷。而之前講過中斷線程可以用Thread.interrupt()或者使用人工的中斷標誌),最主要的幾點是:

  • 捕獲InterruptedException之後應該適當地對它進行處理-大多數情況下適當的處理指的是完全地跳出當前任務/循環/線程。
  • 吞掉InterruptedException不是一個好主意
  • 如果線程在非阻塞調用中被打斷了,這時應該使用isInterrupted()。當線程早已被打斷(也就是被標記為interrupted)時,一旦進入阻塞方法就應該立刻拋出InterruptedException。
  • 轉載自 並發編程網 - ifeve.com

最後更新:2017-05-19 14:33:43

  上一篇:go  《ELK Stack權威指南 》第2章 插件配置
  下一篇:go  數據庫必會必知 之 SQL四種語言:DDL DML DCL TCL