JDK 在線(xiàn)程的 Stop 方法時(shí)明確不得強(qiáng)行銷(xiāo)毀一個(gè)線(xiàn)程,要優(yōu)雅的退出線(xiàn)程。
何謂優(yōu)雅退出線(xiàn)程,即業(yè)務(wù)將進(jìn)行中請(qǐng)求正確被處理,取消待執(zhí)行請(qǐng)求,執(zhí)行資源回收,最終Thread Runable run方法 return 結(jié)束執(zhí)行。
首先問(wèn)為什么要退出一個(gè)線(xiàn)程,再提問(wèn)如何退出一個(gè)線(xiàn)程
1需要線(xiàn)程退出的常見(jiàn)場(chǎng)景
任務(wù)執(zhí)行完成,或異常終止,任務(wù)認(rèn)為無(wú)需再占用線(xiàn)程。
線(xiàn)程池根據(jù)當(dāng)前任務(wù)執(zhí)行情況,伸縮線(xiàn)程池。當(dāng)任務(wù)執(zhí)行較少時(shí),退出空閑的線(xiàn)程。
服務(wù)或進(jìn)程在關(guān)閉階段,例如滾動(dòng)發(fā)布時(shí),需要退出線(xiàn)程、關(guān)閉線(xiàn)程池、關(guān)閉進(jìn)程。
定時(shí)任務(wù)、周期任務(wù)需要終止執(zhí)行時(shí),需要退出當(dāng)前線(xiàn)程。或者退出當(dāng)前任務(wù)的執(zhí)行。
總之既然能創(chuàng)建一個(gè)線(xiàn)程,就會(huì)有退出一個(gè)線(xiàn)程的能力。也會(huì)有退出線(xiàn)程的場(chǎng)景。
關(guān)閉一個(gè)線(xiàn)程的方式分為兩種類(lèi)型:通知線(xiàn)程主動(dòng)關(guān)閉和強(qiáng)行關(guān)閉銷(xiāo)毀線(xiàn)程。
2優(yōu)雅關(guān)閉 or 強(qiáng)行關(guān)閉
實(shí)際上強(qiáng)行關(guān)閉一個(gè)線(xiàn)程,壞處很多,假如要釋放分布式鎖前,突然關(guān)閉線(xiàn)程,那么這個(gè)分布式鎖就無(wú)法釋放。導(dǎo)致后續(xù)正常請(qǐng)求加鎖失敗被阻塞,影響用戶(hù)提單等。強(qiáng)行關(guān)閉一個(gè)線(xiàn)程無(wú)異于給服務(wù)器直接斷電。
3其他語(yǔ)言和 Java 語(yǔ)言退出線(xiàn)程的方式
除了 Java 其他語(yǔ)言如何退出線(xiàn)程呢,實(shí)際上每一種實(shí)現(xiàn)方式都有。例如 C++ 中可以通過(guò) ExitThread、TerminateThread 強(qiáng)行終止線(xiàn)程執(zhí)行。linux 既提供了 pthread_exit C 語(yǔ)言系統(tǒng)調(diào)用強(qiáng)行關(guān)閉線(xiàn)程,也提供了pthread_cancel通知線(xiàn)程關(guān)閉等優(yōu)雅退出方式。
Java 也分別提供優(yōu)雅和強(qiáng)制兩種退出方式,但是目前 JDK 中明確極不推薦強(qiáng)制中斷線(xiàn)程,在Thread.stop()強(qiáng)制中斷線(xiàn)程的注釋中, JDK 這樣解釋
Thread.stop()這種方法本身就是不安全的,Stop 一個(gè)線(xiàn)程會(huì)隨之解鎖這個(gè)線(xiàn)程所持有的監(jiān)視器(可以理解為鎖),如果受這些監(jiān)視器(鎖)保護(hù)的臨界對(duì)象處在不一致?tīng)顟B(tài),則其他線(xiàn)程可能會(huì)看到這些對(duì)象處于不一致?tīng)顟B(tài),那么將導(dǎo)致未知的行為。對(duì)Thread.Stop()的調(diào)用應(yīng)該被簡(jiǎn)單的代碼代替,例如 修改一個(gè)變量,目標(biāo)線(xiàn)程定期檢查這個(gè)變量,有序從 run 方法 return 出來(lái)。如果目標(biāo)線(xiàn)程在一個(gè)條件變量上 wait,則其他線(xiàn)程應(yīng)該使用 interrupt 方法中斷目標(biāo)線(xiàn)程。
實(shí)際上關(guān)閉一個(gè)線(xiàn)程強(qiáng)行和通知是兩種理念,即是否應(yīng)該相信線(xiàn)程任務(wù)的開(kāi)發(fā)者優(yōu)雅的、快速的主動(dòng)退出線(xiàn)程,而不是被其他線(xiàn)程強(qiáng)制終止。在 Java 中,退出線(xiàn)程的方式只有一種推薦,即優(yōu)雅退出,并且 JDK 也給了建議,通過(guò)修改變量,由目標(biāo)線(xiàn)程定期檢查狀態(tài)?;蛘咄ㄟ^(guò) interrupt 中斷方式通知目標(biāo)線(xiàn)程。
下面我們探討下如何優(yōu)雅退出一個(gè)線(xiàn)程?
4優(yōu)雅退出線(xiàn)程
有哪些方式呢?
業(yè)務(wù)字段標(biāo)記
業(yè)務(wù)系統(tǒng)經(jīng)常遇到終止一個(gè)任務(wù)的訴求,例如系統(tǒng)中存在定時(shí)任務(wù),例如外賣(mài)券包在過(guò)期后,未使用的金額,自動(dòng)給用戶(hù)退款。假設(shè)任務(wù)執(zhí)行中,我需要重新制定任務(wù)的入?yún)ⅲ枰冉K止任務(wù)。如何做呢?
大部分任務(wù)類(lèi)代碼都會(huì)循環(huán)處理,例如掃描全表執(zhí)行某個(gè)業(yè)務(wù)邏輯。一定存在循環(huán)處理的場(chǎng)景,可以在循環(huán)入口處判斷任務(wù)是否需要終止執(zhí)行,這樣通過(guò)控制這個(gè)字段,我們就可以終止任務(wù)執(zhí)行。
具體實(shí)施時(shí),可以通過(guò)配置中心控制某一個(gè)任務(wù)是否要終止。
while(config.isTaskEnable()){ //從配置中心獲取任務(wù)是否要終止 //循環(huán)執(zhí)行業(yè)務(wù)邏輯。直到執(zhí)行完成退出,或者被終止。 }
這種退出方式,是告知線(xiàn)程 “你應(yīng)該在合適時(shí)機(jī)退出”, 由線(xiàn)程自己選擇在合適的時(shí)機(jī)檢查該狀態(tài)。那么開(kāi)發(fā)者在設(shè)計(jì)任務(wù)代碼時(shí),就要提前設(shè)計(jì) 合理的退出點(diǎn),在退出點(diǎn)檢查是否需要退出。
Thread.interrupt()
JDK 中提到了如果目標(biāo)線(xiàn)程沒(méi)有處于運(yùn)行態(tài),而是處于阻塞狀態(tài),自然無(wú)法檢查退出的狀態(tài)標(biāo)記,如何通知這個(gè)線(xiàn)程退出呢?
JDK: 如果目標(biāo)線(xiàn)程在一個(gè)條件變量上 wait,則其他線(xiàn)程應(yīng)該使用 interrupt 方法中斷目標(biāo)線(xiàn)程。
interrupt 的 JDK 注釋提到,
如果其他線(xiàn)程調(diào)用目標(biāo)線(xiàn)程的 interrupt 方法,
恰好目標(biāo)線(xiàn)程在調(diào)用。Object.wait(),object.join (),Object.sleep()等方法時(shí),目標(biāo)線(xiàn)程的中斷位標(biāo)記被清除,同時(shí)目標(biāo)線(xiàn)程會(huì)立即從 sleep、wait 等調(diào)用中恢復(fù),并且被拋出InterruptException。
如果目標(biāo)線(xiàn)程在 IO 操作中被阻塞,例如io.channels.InterruptibleChannel,Channel 將被關(guān)閉,線(xiàn)程的中斷位被設(shè)置,同時(shí)目標(biāo)線(xiàn)程收到j(luò)ava.nio.channels.ClosedByInterruptException。
如果目標(biāo)線(xiàn)程被阻塞在java.nio.channels.Selector,線(xiàn)程中斷狀態(tài)被設(shè)置,然后目標(biāo)線(xiàn)程立即從 select 中返回非零值。
如果其他條件都不成立,該線(xiàn)程中斷位會(huì)被設(shè)置。
線(xiàn)程中斷位標(biāo)記了當(dāng)前線(xiàn)程是否處于被中斷狀態(tài),并且提供了Thread.isInterrupted方法查看當(dāng)前是否處于中斷位?那為什么目標(biāo)線(xiàn)程阻塞在Object.wait(),Sleep()方法時(shí),拋出了interruptException,會(huì)取消標(biāo)記呢?實(shí)際上 interrupt 操作執(zhí)行兩件事,1)設(shè)置中斷位標(biāo)記 2)通過(guò) unpark 喚醒目標(biāo)線(xiàn)程(park 和 unpark 分別可以阻塞線(xiàn)程和喚醒線(xiàn)程)。
然而目標(biāo)線(xiàn)程醒來(lái)時(shí)會(huì)檢查當(dāng)前是否處于中斷位,如果是 sleep 或者 wait 操作。如果處于中斷位則取消中斷位,拋出異常。取消中段位的原因應(yīng)該是一種規(guī)范,即拋出中斷異常,即通知了線(xiàn)程中斷,無(wú)需再用中段位標(biāo)記。
其他場(chǎng)景 2、場(chǎng)景 3 在被喚醒后,分別執(zhí)行對(duì)應(yīng)的中斷響應(yīng)策略。
interrupt 中斷邏輯是確定的,業(yè)務(wù)線(xiàn)程要考慮自己是否調(diào)用了 sleep、wait 或者 io、selector 等操作,根據(jù)不同的場(chǎng)景,選擇自己合適的中斷響應(yīng)策略。
那么推薦業(yè)務(wù)線(xiàn)程如何響應(yīng)中斷呢?
推薦的中斷響應(yīng)策略
立即響應(yīng)中斷
目標(biāo)線(xiàn)程的任務(wù)在InterruptedException異常處理中,要主動(dòng)回收資源,打印日志,退出任務(wù)執(zhí)行。
目標(biāo)線(xiàn)程如果沒(méi)有阻塞操作,例如 sleep、wait。可以通過(guò)Thread.isInterrupted(),查看當(dāng)前中斷位狀態(tài),如果被中斷了,則采取以上第一步操作。
忽略中斷,交給上一層處理
所謂上一層,可以理解為是調(diào)用堆棧的上一層,例如本層代碼不負(fù)責(zé)處理中斷這個(gè)場(chǎng)景,那么 Interrupt 異常被拋出后,可以選擇如何方案:
拋出InterruptedException給上層,由上層代碼處理。
調(diào)用Thread.interrupt()。重新設(shè)置中斷位標(biāo)記 (自己中斷自己)。由上游代碼在本層方法返回后,檢查中斷位標(biāo)記,進(jìn)行中斷處理。
當(dāng)然最推薦的方式還是拋出InterruptedException,讓上游感知到下游調(diào)用鏈中存在阻塞,讓上游對(duì)中斷異常進(jìn)行處理。
千萬(wàn)不要吞掉中斷
什么是吞掉中斷?例如當(dāng) sleep 拋出InterruptedException后,忽略異常,不執(zhí)行任何操作,繼續(xù)執(zhí)行業(yè)務(wù)邏輯。
for(inti=0;i如果這樣處理,中斷異常被忽略,中斷標(biāo)記位也被忽略。即便上游方法對(duì)中斷有處理策略,也無(wú)法感知到中斷。例如上游調(diào)用可能會(huì)判斷。
while(true){
callChildMethod();//調(diào)用下游方法,但是下游吞掉了中斷 if(Thread.currentThread().isInterrupted()){ //回收資源,退出線(xiàn)程 } }有人會(huì)問(wèn),既然上層都能知道處理中斷,為什么下層方法開(kāi)發(fā)者會(huì)不記得拋出中斷或重置中斷位呢?
因?yàn)樯舷聝蓪?,很可能不是一個(gè)開(kāi)發(fā)者。例如上層是通用的框架代碼,定義了任務(wù)的指定邏輯,提供了擴(kuò)展點(diǎn)方法,下游只需要實(shí)現(xiàn)擴(kuò)展方法即可。但是另一個(gè)開(kāi)發(fā)者在實(shí)現(xiàn)擴(kuò)展點(diǎn)方法時(shí),吞掉了中斷異常,導(dǎo)致本來(lái)框架層已經(jīng)處理好中斷了,但還是無(wú)法響應(yīng)中斷。
所以中斷的響應(yīng)是需要上下層,每一層代碼邏輯都需要考慮的事情。就算框架層處理好中斷異常處理,業(yè)務(wù)邏輯層也要關(guān)注中斷處理。
最后提醒一下,Thread.interrupted方法會(huì)返回當(dāng)前中斷標(biāo)記,并且取消中斷位。如果只查詢(xún)中斷位,不想清理,可以使用Thread.isInterrupted()。
5總結(jié)
不推薦強(qiáng)制銷(xiāo)毀線(xiàn)程,會(huì)導(dǎo)致資源無(wú)法被釋放,進(jìn)行中請(qǐng)求無(wú)法正常處理完,導(dǎo)致業(yè)務(wù)數(shù)據(jù)處于不可知的狀態(tài)。
Java 推薦優(yōu)雅退出線(xiàn)程。
業(yè)務(wù)層可以使用字段標(biāo)記,定期檢查是否需要退出任務(wù)。
Thread.interrupt中斷目標(biāo)線(xiàn)程、isInterrupted查詢(xún)中斷位標(biāo)記。
使用Thread.interrupt處理中斷也可以?xún)?yōu)雅退出,但需要上下層堆棧都要關(guān)注中斷,不得吞掉中斷。
編輯:黃飛
-
JAVA
+關(guān)注
關(guān)注
19文章
2976瀏覽量
105207 -
C語(yǔ)言
+關(guān)注
關(guān)注
180文章
7614瀏覽量
137817 -
C++
+關(guān)注
關(guān)注
22文章
2114瀏覽量
73882 -
JDK
+關(guān)注
關(guān)注
0文章
82瀏覽量
16637 -
線(xiàn)程
+關(guān)注
關(guān)注
0文章
505瀏覽量
19762
原文標(biāo)題:JDK 推薦的線(xiàn)程關(guān)閉方式
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
【RT-Thread學(xué)習(xí)筆記】如何優(yōu)雅地退出QEMU模擬器?
![【RT-Thread學(xué)習(xí)筆記】如何<b class='flag-5'>優(yōu)雅</b>地<b class='flag-5'>退出</b>QEMU模擬器?](https://file.elecfans.com/web2/M00/56/6F/pYYBAGLfRjOAcPKBAAFd6YnXym4812.png)
RTThread中main線(xiàn)程有個(gè)循環(huán),如果main線(xiàn)程異常退出了,有什么辦法可以監(jiān)測(cè)到?
多線(xiàn)程退出機(jī)制初探
LWIP相關(guān)的線(xiàn)程是如何通過(guò)一個(gè)信號(hào)控制創(chuàng)建的呢
教你一種如何優(yōu)雅地退出QEMU模擬器的方法
java jdk6.0官方下載
![java <b class='flag-5'>jdk</b>6.0官方下載](https://file.elecfans.com/web2/M00/49/68/pYYBAGKhtEyAa30yAAAPLnCbRWE781.jpg)
多線(xiàn)程編程之Linux線(xiàn)程編程
JDK 19 / Java 19正式發(fā)布 虛擬線(xiàn)程來(lái)了
細(xì)數(shù)線(xiàn)程池的10個(gè)坑
![細(xì)數(shù)<b class='flag-5'>線(xiàn)程</b>池的10<b class='flag-5'>個(gè)</b>坑](https://file1.elecfans.com/web2/M00/89/D4/wKgaomSLxTmAX_s0AAAR9v5rHaQ835.png)
線(xiàn)程池的兩個(gè)思考
![<b class='flag-5'>線(xiàn)程</b>池的兩<b class='flag-5'>個(gè)</b>思考](https://file1.elecfans.com/web2/M00/A5/D8/wKgaomUOn-iALOf-AAHMZF-WGOk441.jpg)
如何使用JDK截?cái)?b class='flag-5'>一個(gè)字符串
JDK中常見(jiàn)的Lamada表達(dá)式
![<b class='flag-5'>JDK</b>中常見(jiàn)的Lamada表達(dá)式](https://file1.elecfans.com/web2/M00/A7/8D/wKgaomUk-BCABJCvAAAfKtBRuAM430.jpg)
如何查看一個(gè)線(xiàn)程的ID
![如何查看<b class='flag-5'>一</b><b class='flag-5'>個(gè)</b><b class='flag-5'>線(xiàn)程</b>的ID](https://file1.elecfans.com/web2/M00/AF/BE/wKgZomVRwx-AF1TqAABtFQA_Xxs411.jpg)
評(píng)論