當前位置: 妍妍網 > 碼農

原來,這才是 JDK 推薦的執行緒關閉方式

2024-05-27碼農

點選關註公眾號,Java幹貨 及時送達

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

    精品資料,超贊福利,免費領

    微信掃碼/長按辨識 添加【技術交流群

    群內每天分享精品學習資料

    最近開發整理了一個用於速刷面試題的小程式;其中收錄了上千道常見面試題及答案(包含基礎並行JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud訊息佇列等多個型別),歡迎您的使用。

    👇👇

    👇點選"閱讀原文",獲取更多資料(持續更新中