點選「 IT碼徒 」, 關註,置頂 公眾號
每日技術幹貨,第一時間送達!
J DK 線上程的 Stop 方法時明確不得強行銷毀一個執行緒,要優雅的結束執行緒。
何謂優雅結束執行緒,即業務將進行中請求正確被處理,取消待執行請求,執行資源回收,最終 Thread Runable run 方法 return 結束執行。
首先問為什麽要結束一個執行緒,再提問如何結束一個執行緒
需要執行緒結束的常見場景
任務執行完成,或異常終止,任務認為無需再占用執行緒。
執行緒池根據當前任務執行情況,伸縮執行緒池。當任務執行較少時,結束空閑的執行緒。
服務或行程在關閉階段,例如捲動釋出時,需要結束執行緒、關閉執行緒池、關閉行程。
定時任務、周期任務需要終止執行時,需要結束當前執行緒。或者結束當前任務的執行。
總之既然能建立一個執行緒,就會有結束一個執行緒的能力。也會有結束執行緒的場景。
關閉一個執行緒的方式分為兩種型別:通知執行緒主動關閉和強行關閉銷毀執行緒。
優雅關閉 or 強行關閉
實際上強行關閉一個執行緒,壞處很多,假如要釋放分布式鎖前,突然關閉執行緒,那麽這個分布式鎖就無法釋放。導致後續正常請求加鎖失敗被阻塞,影響使用者提單等。
強行關閉一個執行緒無異於給伺服器直接斷電。
其他語言和 Java 語言結束執行緒的方式
除了 Java 其他語言如何結束執行緒呢,實際上每一種實作方式都有。例如 C++ 中可以透過 ExitThread、TerminateThread 強行終止執行緒執行。linux 既提供了 pthread_exit C 語言系統呼叫強行關閉執行緒,也提供了 pthread_cancel 通知執行緒關閉等優雅結束方式。
Java 也分別提供優雅和強制兩種結束方式,但是目前 JDK 中明確極不推薦強制中斷執行緒,在 Thread。stop() 強制中斷執行緒的註釋中, JDK 這樣解釋
Thread.stop() 這種方法本身就是不安全的,Stop 一個執行緒會隨之解鎖這個執行緒所持有的監視器(可以理解為鎖),如果受這些監視器(鎖)保護的臨界物件處在不一致狀態,則其他執行緒可能會看到這些物件處於不一致狀態,那麽將導致未知的行為。 對 Thread.Stop() 的呼叫應該被簡單的程式碼代替,例如 修改一個變量,目標執行緒定期檢查這個變量,有序從 run 方法 return 出來。如果目標執行緒在一個條件變量上 wait,則其他執行緒應該使用 interrupt 方法中斷目標執行緒。
實際上關閉一個執行緒強行和通知是兩種理念,即是否應該相信執行緒任務的開發者優雅的、快速的主動結束執行緒,而不是被其他執行緒強制終止。在 Java 中,結束執行緒的方式只有一種推薦,即優雅結束,並且 JDK 也給了建議,透過修改變量,由目標執行緒定期檢查狀態。或者透過 interrupt 中斷方式通知目標執行緒。
下面我們探討下如何優雅結束一個執行緒?
優雅結束執行緒
有哪些方式呢?
業務欄位標記
業務系統經常遇到終止一個任務的訴求,例如系統中存在定時任務,例如外賣券包在過期後,未使用的金額,自動給使用者退款。假設任務執行中,我需要重新制定任務的入參,需要先終止任務。如何做呢?大部份任務類程式碼都會迴圈處理,例如掃描全表執行某個業務邏輯。一定存在迴圈處理的場景,可以在迴圈入口處判斷任務是否需要終止執行,這樣透過控制這個欄位,我們就可以終止任務執行。
具體實施時,可以透過配置中心控制某一個任務是否要終止。
while(config.isTaskEnable()) {
//從配置中心獲取任務是否要終止
//迴圈執行業務邏輯。直到執行完成結束,或者被終止。
}
這種結束方式,是告知執行緒 「你應該在合適時機結束」, 由執行緒自己選擇在合適的時機檢查該狀態。那麽開發者在設計任務程式碼時,就要提前設計 合理的結束點,在結束點檢查是否需要結束。
Thread.interrupt()
JDK 中提到了如果目標執行緒沒有處於執行態,而是處於阻塞狀態,自然無法檢查結束的狀態標記,如何通知這個執行緒結束呢?
JDK: 如果目標執行緒在一個條件變量上 wait,則其他執行緒應該使用 interrupt 方法中斷目標執行緒。
interrupt 的 JDK 註釋提到,
如果其他執行緒呼叫目標執行緒的 interrupt 方法,
恰好目標執行緒在呼叫。Object.wait(),object.join (),Object.sleep() 等方法時,目標執行緒的中斷位標記被清除,同時目標執行緒會立即從 sleep、wait 等呼叫中恢復,並且被丟擲 InterruptException。
如果目標執行緒在 IO 操作中被阻塞,例如 io.channels.InterruptibleChannel,Channel 將被關閉,執行緒的中斷位被設定,同時目標執行緒收到 java.nio.channels.ClosedByInterruptException。
如果目標執行緒被阻塞在 java.nio.channels.Selector,執行緒中斷狀態被設定,然後目標執行緒立即從 select 中返回非零值。
如果其他條件都不成立,該執行緒中斷位會被設定。
執行緒中斷位標記了當前執行緒是否處於被中斷狀態,並且提供了 Thread.isInterrupted 方法檢視當前是否處於中斷位?那為什麽目標執行緒阻塞在 Object.wait(),Sleep() 方法時,丟擲了 interruptException,會取消標記呢?實際上 interrupt 操作執行兩件事,1)設定中斷位標記 2)透過 unpark 喚醒目標執行緒 (park 和 unpark 分別可以阻塞執行緒和喚醒執行緒) 。
然而目標執行緒醒來時會檢查當前是否處於中斷位,如果是 sleep 或者 wait 操作。如果處於中斷位則取消中斷位,丟擲異常。取消中段位的原因應該是一種規範,即丟擲中斷異常,即通知了執行緒中斷,無需再用中段位標記。
其他場景 2、場景 3 在被喚醒後,分別執行對應的中斷響應策略。
interrupt 中斷邏輯是確定的,業務執行緒要考慮自己是否呼叫了 sleep、wait 或者 io、selector 等操作,根據不同的場景,選擇自己合適的中斷響應策略。
那麽推薦業務執行緒如何響應中斷呢?
推薦的中斷響應策略
立即響應中斷
目標執行緒的任務在 InterruptedException 例外處理中,要主動回收資源,打印日誌,結束任務執行。
目標執行緒如果沒有阻塞操作,例如 sleep、wait。可以透過 Thread.isInterrupted(),檢視當前中斷位狀態,如果被中斷了,則采取以上第一步操作。
忽略中斷,交給上一層處理
所謂上一層,可以理解為是呼叫堆疊的上一層,例如本層程式碼不負責處理中斷這個場景,那麽 Interrupt 異常被丟擲後,可以選擇如何方案:
丟擲 InterruptedException 給上層,由上層程式碼處理。
呼叫 Thread.interrupt()。重新設定中斷位標記 (自己中斷自己)。由上遊程式碼在本層方法返回後,檢查中斷位標記,進行中斷處理。
當然最推薦的方式還是丟擲 InterruptedException,讓上遊感知到下遊呼叫鏈中存在阻塞,讓上遊對中斷異常進行處理。
千萬不要吞掉中斷
什麽是吞掉中斷?例如當 sleep 丟擲 InterruptedException 後,忽略異常,不執行任何操作,繼續執行業務邏輯。
for (int i = 0; i < cnt; i++) {
try {
//執行業務邏輯
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("被中斷");
}
System.out.println("子執行緒執行中");
}
如果這樣處理,中斷異常被忽略,中斷標記位也被忽略。即便上遊方法對中斷有處理策略,也無法感知到中斷。例如上遊呼叫可能會判斷。
while(true){
callChildMethod();//呼叫下遊方法,但是下遊吞掉了中斷
if (Thread.currentThread().isInterrupted()) {
//回收資源,結束執行緒
}
}
有人會問,既然上層都能知道處理中斷,為什麽下層方法開發者會不記得丟擲中斷或重設中斷位呢?
因為上下兩層,很可能不是一個開發者。例如上層是通用的框架程式碼,定義了任務的指定邏輯,提供了擴充套件點方法,下遊只需要實作擴充套件方法即可。但是另一個開發者在實作擴充套件點方法時,吞掉了中斷異常,導致本來框架層已經處理好中斷了,但還是無法響應中斷。
所以中斷的響應是需要上下層,每一層程式碼邏輯都需要考慮的事情。就算框架層處理好中斷例外處理,業務邏輯層也要關註中斷處理。
最後提醒一下,Thread.interrupted 方法會返回當前中斷標記,並且取消中斷位。如果只查詢中斷位,不想清理,可以使用 Thread.isInterrupted()。
總結
不推薦強制銷毀執行緒,會導致資源無法被釋放,進行中請求無法正常處理完,導致業務數據處於不可知的狀態。
Java 推薦優雅結束執行緒。
業務層可以使用欄位標記,定期檢查是否需要結束任務。
Thread.interrupt 中斷目標執行緒、isInterrupted 查詢中斷位標記。
使用 Thread.interrupt 處理中斷也可以優雅結束,但需要上下層堆疊都要關註中斷,不得吞掉中斷。
連結:juejin.cn/post/7291564831710445622
—
END
—
PS:防止找不到本篇文章,可以收藏點贊,方便翻閱尋找哦。
往期推薦