樸實的聊聊很多人會誤解/不懂的Java併發中斷機制

日拱一兵發表於2020-05-20

| 好看請贊,養成習慣

  • 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo程式碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀檢視,本文同樣收錄在此,覺得不錯,還請Star?


橫看成嶺側成峰,遠近高低各不同,併發程式設計理論系列基本已經結束,相信大家有了理論的鋪墊,近看原始碼才能發現其設計之美,不會一頭霧水


本來是要介紹 AQS 作為我們走進併發程式設計原始碼環節的第一步,但 AQS 涉及的知識點也還真有點多,每一個都夠單獨拿出來說一說,恰巧有朋友私信我“不理解執行緒的中斷機制”,中斷機制又恰巧是 AQS API實現的一部分,更貫穿於整個併發程式設計內容中。於是就打算單獨說一說這個小機制,先讓大家做到心中有 number

在學習/編寫併發程式時,總會聽到/看到如下詞彙:

  • 執行緒被中斷或丟擲InterruptedException
  • 設定了中斷標識
  • 清空了中斷標識
  • 判斷執行緒是否被中斷

在 Java Thread 類又提供了長相酷似,讓人傻傻分不清的三個方法來處理併發中斷問題:

  • interrupt()
  • interrupted()
  • isInterrupted()

看到這我不禁會問自己:

什麼是中斷機制?

剛剛接觸【中斷】這個詞時,先入為主的概念就是“直接中斷/打斷”正在做的事,使其停止。我的理解是這樣的:

你:在打遊戲

女朋友:別打遊戲了,趕快過來吃飯

你:聽到女朋友招呼之後立馬中斷手中的遊戲乖乖過去吃飯

在多執行緒程式設計中,中斷是一種【協同】機制,怎麼理解這麼高大上的詞呢?就是女朋友叫你吃飯,你收到了中斷遊戲通知,但是否馬上放下手中的遊戲去吃飯看你心情 。在程式中怎樣演繹這個心情就看具體的業務邏輯了,Java 的中斷機制就是這麼簡單

如果還沒改變這個先入為主的概念,我懷你你沒有女朋友(?)我們擁抱一下

為什麼會有中斷機制?

中斷是一種協同機制,我覺得就是解決【當局者迷】的狀況

現實中,你努力忘我沒有晝夜的工作,如果再沒有人告知你中斷,你身體是吃不消的。

在多執行緒的場景中,有的執行緒可能迷失在怪圈無法自拔(自旋浪費資源),這時就可以用其他執行緒在恰當的時機給它箇中斷通知,被“中斷”的執行緒可以選擇在恰當的時機選擇跳出怪圈,最大化的利用資源

那程式中如何中斷?怎樣識別是否中斷?又如何處理中斷呢?這就與上文提到的三個方法有關了

interrupt() VS isInterrupted() VS interrupted()

Java 的每個執行緒物件裡都有一個 boolean 型別的標識,代表是否有中斷請求,可你尋遍 Thread 類你也不會找到這個標識,因為這是通過底層 native 方法實現的。

interrupt()

interrupt() 方法是 唯一一個 可以將上面提到中斷標誌設定為 true 的方法,從這裡可以看出,這是一個 Thread 類 public 的物件方法,所以可以推斷出任何執行緒物件都可以呼叫該方法,進一步說明就是可以一個執行緒 interrupt 其他執行緒,也可以 interrupt 自己。其中,中斷標識的設定是通過 native 方法 interrupt0 完成的

在 Java 中,執行緒被中斷的反應是不一樣的,脾氣不好的直接就丟擲了 InterruptedException()

該方法註釋上寫的很清楚,當執行緒被阻塞在:

  1. wait()
  2. join()
  3. sleep()

這些方法時,如果被中斷,就會丟擲 InterruptedException 受檢異常(也就是必須要求我們 catch 進行處理的)

熟悉 JUC 的朋友可能知道,其實被中斷丟擲 InterruptedException 的遠遠不止這幾個方法,比如:

反向推理,這些可能阻塞的方法如果宣告有 throws InterruptedException , 也就暗示我們它們是可中斷的

呼叫 interrput() 方法後,中斷標識就被設定為 true 了,那我們怎麼利用這個中斷標識,來判斷某個執行緒中斷標識到底什麼狀態呢?

isInterrupted()

這個方法名起的非常好,因為比較符合我們 bean boolean 型別欄位的 get 方法規範,沒錯,該方法就是返回中斷標識的結果:

  • true:執行緒被中斷,
  • false:執行緒沒被中斷或被清空了中斷標識(如何清空我們一會看)

拿到這個標識後,執行緒就可以判斷這個標識來執行後續的邏輯了。有起名好的,也有起名不好的,就是下面這個方法:

interrupted()

按照常規翻譯,過去時時態,這就是“被打斷了/被打斷的”,其實和上面的 isInterrupted() 方法差不多,兩個方法都是呼叫 private 的 isInterrupted() 方法, 唯一差別就是會清空中斷標識(這是從方法名中怎麼也看不出來的)

因為呼叫該方法,會返回當前中斷標識,同時會清空中斷標識,就有了那一段有點讓人迷惑的方法註釋:

來段程式你就會明白上面註釋的意思了:

Thread.currentThread().isInterrupted(); // true
Thread.interrupted() // true,返回true後清空了中斷標識將其置為 false
Thread.currentThread().isInterrupted(); // false
Thread.interrupted() // false

這個方法總覺得很奇怪,現實中有什麼用呢?

當你可能要被大量中斷並且你想確保只處理一次中斷時,就可以使用這個方法了

該方法在 JDK 原始碼中應用也非常多,比如(後續文章會具體分析,這裡知道該方法的作用和使用場景就好):

相信到這裡你已經能明確分辨三胞胎都是誰,併發揮怎樣的作用了,那麼有哪些場景我們可以使用中斷機制呢?

中斷機制的使用場景

通常,中斷的使用場景有以下幾個

  • 點選某個桌面應用中的關閉按鈕時(比如你關閉 IDEA,不儲存資料直接中斷好嗎?);
  • 某個操作超過了一定的執行時間限制需要中止時;
  • 多個執行緒做相同的事情,只要一個執行緒成功其它執行緒都可以取消時;
  • 一組執行緒中的一個或多個出現錯誤導致整組都無法繼續時;

因為中斷是一種協同機制,提供了更優雅中斷方式,也提供了更多的靈活性,所以當遇到如上場景等,我們就可以考慮使用中斷機制了

使用中斷機制有哪些注意事項

其實使用中斷機制無非就是注意上面說的兩項內容:

  1. 中斷標識
  2. InterruptedException

前浪已經將其總結為兩個通用原則,我們後浪直接站在肩膀上用就可以了,來看一下這兩個原則是什麼:

原則-1

如果遇到的是可中斷的阻塞方法, 並丟擲 InterruptedException,可以繼續向方法呼叫棧的上層丟擲該異常;如果檢測到中斷,則可清除中斷狀態並丟擲 InterruptedException,使當前方法也成為一個可中斷的方法

原則-2

若有時候不太方便在方法上丟擲 InterruptedException,比如要實現的某個介面中的方法簽名上沒有 throws InterruptedException,這時就可以捕獲可中斷方法的 InterruptedException 並通過 Thread.currentThread.interrupt() 來重新設定中斷狀態。

再通過個例子來加深一下理解:

本意是當前執行緒被中斷之後,退出while(true), 你覺得程式碼有問題嗎?(先不要向下看)

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
  // 省略業務程式碼
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}

開啟 Thread.sleep 方法:

sleep 方法丟擲 InterruptedException後,中斷標識也被清空置為 false,我們在catch 沒有通過呼叫 th.interrupt() 方法再次將中斷標識置為 true,這就導致無限迴圈了

這兩個原則很好理解。總的來說,我們應該留意 InterruptedException,當我們捕獲到該異常時,絕不可以默默的吞掉它,什麼也不做,因為這會導致上層呼叫棧什麼資訊也獲取不到。其實在編寫程式時,捕獲的任何受檢異常我們都不應該吞掉

JDK 中有哪些使用中斷機制的地方呢?

中斷機制貫穿整個併發程式設計中,這裡只簡單列覺大家經常會使用的,我們可以通過閱讀JDK原始碼來進一步瞭解中斷機制以及學習如何使用中斷機制

ThreadPoolExecutor

ThreadPoolExecutor 中的 shutdownNow 方法會遍歷執行緒池中的工作執行緒並呼叫執行緒的 interrupt 方法來中斷執行緒

FutureTask

FutureTask 中的 cancel 方法,如果傳入的引數為 true,它將會在正在執行非同步任務的執行緒上呼叫 interrupt 方法,如果正在執行的非同步任務中的程式碼沒有對中斷做出響應,那麼 cancel 方法中的引數將不會起到什麼效果

總結

到這裡你應該理解Java 併發程式設計中斷機制的含義了,它是一種協同機制,和你先入為主的概念完全不一樣。區分了三個相近方法,說明了使用場景以及使用原則,同時又給出JDK原始碼一些常見案例,相信你已經胸中有溝壑了,接下來,跟上節奏,我們陸續走進原始碼吧

靈魂追問

  1. 丟擲 InterruptedException 後,中斷標識就一定被清空嗎?
  2. 處在死鎖狀態的執行緒是否可以被中斷呢?
  3. 進入臨界區的執行緒能否被中斷呢?如果不能有什麼辦法能響應中斷嗎?
  4. 個人感覺interrupted這個方法名稱不是特別好,如果你也覺得不好,讓你設計這個地方,你有什麼想法?

有朋友可能會問文章開頭的圖,同時看一個類的不同部分怎麼實現的?不等您開口,我就全盤的招了,其實就是螢幕分割(在檔案上滑鼠右鍵->選擇水平/垂直分割),這樣在同時檢視某些程式碼時還是很方便的(帶魚屏垂直分割真是爽翻天),保姆式演示如下:

參考

  1. Java 併發程式設計實戰
  2. Java併發程式設計的藝術
  3. https://www.infoq.cn/article/java-interrupt-mechanism
  4. https://coderanch.com/t/237332/certification/explain-interrupt-isInterrupted-interrupted-method
  5. https://dzone.com/articles/waiting-for-coroutines

相關文章