領(lǐng)導(dǎo)說誰再用Stop直接下崗,這樣終止線程更優(yōu)雅
本文收錄于《Java并發(fā)編程》合集,本文主要介紹Java并發(fā)編程中終止線程的手段,通過本文您可以了解到:
- 通過Thread類提供的方法中斷線程
- 中斷線程的應(yīng)用場景和代碼實(shí)現(xiàn),以及實(shí)現(xiàn)中的細(xì)節(jié)處理
- stop方法中斷線程存在的隱患
- LockSupport停止和喚醒線程
- LockSupport工具類的park和unpark的原理
原本的Java線程Thread類API中提供了stop這樣的終止線程的方法,但是已被標(biāo)記為過時方法,此方法來終止線程是暴力的不安全的,沒有對線程做后續(xù)的善后操作而直接終止,往往會埋下一些隱患。我們可以通過Java線程的中斷機(jī)制,來安全的停止線程。
Java提供了線程的中斷機(jī)制:設(shè)置線程的中斷標(biāo)志,可以使用它來決定是否結(jié)束一個線程。通過設(shè)置線程的中斷標(biāo)志并不能直接終止線程,這種機(jī)制其實(shí)就是告知線程我希望打斷你,至于到底停止不停止是由線程決定。
打斷線程場景
比如打斷或者重新執(zhí)行一些耗時過長任務(wù),多線程同時完成同一個相同任務(wù),某一線程如果執(zhí)行完就通知其他線程可以停止。比如:
- 下載任務(wù),發(fā)現(xiàn)需要耗時過長,可以直接取消下載,其實(shí)就是打斷這個下載數(shù)據(jù)線程
- 比如搶票軟件,開啟多個線程搶多個車次的車票,如果某一個線程搶到,就通知其他線程可以終止
- 比如服務(wù)器中的超時操作,長時間沒有獲取到數(shù)據(jù),就終止線程
你在哪里使用過打斷線程呢?
Thread類相關(guān)API
- void interrupt():中斷線程,例如線程A運(yùn)行時,線程B調(diào)用線程A的interrupt方法來設(shè)置線程A的中斷標(biāo)志為true。注意:這里僅僅是設(shè)置了標(biāo)志,線程A并沒有中斷,它會繼續(xù)往下執(zhí)行。如果線程A調(diào)用了wait,join,sleep方法而被阻塞掛起,如果此時調(diào)用線程A的interrupt()方法,線程A會在調(diào)用這些方法的地方拋出InterruptedException異常而返回。
- boolean isInterrupted():檢測當(dāng)前線程是否被中斷,如果是返回true,否則返回false。
- boolean interrupted():檢測當(dāng)前線程是否被中斷,這個方法是Thread類的靜態(tài)方法;與interrupt()方法的區(qū)別在于,如果發(fā)現(xiàn)當(dāng)前線程被中斷,則會清除中斷標(biāo)志。
另外需要注意的是:interrupted()方法是獲取當(dāng)前調(diào)用線程【正在運(yùn)行的線程】的中斷標(biāo)志,而不是調(diào)用interrupted()方法的線程的中斷標(biāo)志。
打斷線程
運(yùn)行結(jié)果:
- 調(diào)用 interrupted方法之后如果被打斷的線程【t1】線程中調(diào)用sleep、wait、join方法會觸發(fā)異常
- 調(diào)用isInterrupted方法獲取打斷標(biāo)記,true為打斷,false為未打斷
線程中調(diào)用wait方法
運(yùn)行結(jié)果:
- 調(diào)用wait方法之后仍然會觸發(fā)異常
- 打斷標(biāo)記被重置變?yōu)閒alse,所以可以在catch塊中設(shè)置打斷標(biāo)記為true
終止線程
此時可以根據(jù)打斷標(biāo)記,在線程內(nèi)部判斷是否需要打斷
案例:小明放假回家,媽媽想著給小明做好吃的,但是小明失戀了沒有胃口,就給媽媽說不要做了,媽媽收到打斷消息之后就停止做飯。
運(yùn)行結(jié)果:媽媽線程做飯,當(dāng)小明線程打斷之后,媽媽線程就收到打斷信息,停止運(yùn)行
通過代碼我們發(fā)現(xiàn),其實(shí)線程的終止權(quán)在被打斷的線程中,通過判斷打斷標(biāo)記來控制是否終止,也就是小明不讓媽媽做飯,媽媽可以不做也可以繼續(xù)做。
停止線程其他方法
- 使用Thread類中的stop()方法
- 調(diào)用 stop() 方法會立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 語句中的,并拋出ThreadDeath異常(通常情況下此異常不需要顯示的捕獲),因此可能會導(dǎo)致一些清理性的工作的得不到完成,如關(guān)閉文件數(shù)據(jù)流,關(guān)閉數(shù)據(jù)庫連接等。
- 調(diào)用 stop() 方法會立即釋放該線程所持有的所有的鎖,導(dǎo)致數(shù)據(jù)得不到同步,出現(xiàn)數(shù)據(jù)不一致的問題。
- 使用System.exit(int)方法
- 該方法會直接停止JVM,不單單停止一個線程,殺傷力太大
線程終止案例
小明是大強(qiáng)的秘書,負(fù)責(zé)記錄會議內(nèi)容,小明會每1秒記錄一次重要講話內(nèi)容,當(dāng)會議結(jié)束后,就終止記錄工作,但是在終止時會再整理一下會議內(nèi)容
會議線程:
測試類:
運(yùn)行結(jié)果:
如果沒有在catch中重新打斷線程,則會不斷記錄下去,記?。寒?dāng)線程中調(diào)用了sleep,wait,join方法時,線程的打斷標(biāo)記會被重置,需要在catch塊中重新打斷
isInterrupted 和 interrupted區(qū)別
文章開頭介紹過,兩個方法都是判斷線程的打斷狀態(tài),interrupted 是Thread類的靜態(tài)方法,獲取打斷狀態(tài)之后會重置打斷狀態(tài)為false,而isInterrupted是Thread對象的方法,非靜態(tài)方法不會重置打斷狀態(tài)
isInterrupted 方法
interrupted方法
發(fā)現(xiàn)查看小明線程狀態(tài)時已經(jīng)變?yōu)?false
注意:Thread.interrupted查看的是正在執(zhí)行的線程的狀態(tài),而isInterrupted是可以指定線程的,根據(jù)線程變量名查看狀態(tài)
LockSupport
LockSupport是JUC中一個工具類,構(gòu)造方法私有,并且不存在獲取實(shí)例的靜態(tài)方法,提供了一堆靜態(tài)方法幫助完成對線程的操作,主要是為了暫停和恢復(fù)線程,主要方法有兩個:
- park():暫停線程
- unpark(Thread thread):恢復(fù)指定線程對象運(yùn)行
park停止當(dāng)前線程
運(yùn)行結(jié)果:發(fā)現(xiàn)線程執(zhí)行啟動后就停止,因?yàn)橛龅搅薒ockSupport.park();導(dǎo)致t1線程停止運(yùn)行,但是線程還未結(jié)束,所以程序并未停止【左側(cè)紅色小框標(biāo)識】
打斷park線程:如下圖,在main線程中,等待 1秒 之后,執(zhí)行t1.interrupt();,打斷t1線程,此時t1線程已經(jīng)被停止運(yùn)行,發(fā)現(xiàn)調(diào)用中斷方法之后,t1線程又繼續(xù)執(zhí)行,并且打斷標(biāo)記為true
park細(xì)節(jié)
如果park之后,阻塞線程,繼續(xù)執(zhí)行,再次調(diào)用LockSupport.park();方法阻塞線程時無效,如下進(jìn)行阻塞二次:
運(yùn)行結(jié)果:
如果你還想再次打斷,可以調(diào)用一次 Thread.interrupted() 獲取線程打斷狀態(tài),并且再設(shè)置為false,發(fā)現(xiàn)已成功阻塞
unpark:恢復(fù)指定線程對象
- t1線程執(zhí)行2S后被阻塞
- 主線程中等待1S后喚醒t1線程
執(zhí)行后發(fā)現(xiàn):t1線程被正常喚醒,繼續(xù)執(zhí)行
如果我們調(diào)換一下阻塞和喚醒的順序,是否仍然可以正常喚醒呢?也就是:先喚醒t1線程,t1線程再進(jìn)入阻塞狀態(tài),發(fā)現(xiàn)仍然可以喚醒成功
為什么可以先調(diào)用unpark在調(diào)用park呢?
這是一道高頻面試題,在下邊也有總結(jié),在這里我們不妨先分析一下,這涉及到了 park和unpark原理
當(dāng)我們調(diào)用park方法時其實(shí)調(diào)用的是 sun.misc.Unsafe UNSAFE 類的 park 方法,這里提到【許可證】這個詞,就是park和unpark的關(guān)鍵
每個線程都有一個自己的Parker對象,該對象由三部分組成_counter、_cond、_mutex
可以想一下,unpark其實(shí)就相當(dāng)于一個許可,告訴特定工廠你可以繼續(xù)生產(chǎn),特定工廠想要park停止生產(chǎn)的時候一看到有許可,就可以會繼續(xù)運(yùn)行。因此其執(zhí)行順序可以顛倒。
Parker實(shí)例:
park方法:
當(dāng)調(diào)用park()時,先嘗試能否直接拿到【許可】,即_counter>0,如果獲取成功,則把_counter設(shè)置為0,并返回
如果不成功,則構(gòu)造一個ThreadBlockInVM,然后檢查_counter是不是>0,如果是,則把_counter設(shè)置為0,并返回
否則,再判斷等待的時間,然后再調(diào)用pthread_cond_wait函數(shù)等待,如果等待返回,則把_counter設(shè)置為0并返回:
這就是整個park的過程,總結(jié)來說就是消耗【許可】的過程。
unpark方法
JDK源碼unpark方法其實(shí)也是調(diào)用了sun.misc.Unsafe UNSAFE類的unpark方法,注釋的意思是給線程【許可證】
當(dāng)調(diào)用unpark()時,直接設(shè)置_counter為1,如果_counter之前的值是0,則還要調(diào)用pthread_cond_signal喚醒在park中等待的線程:
_counter的值最大為1,即使多次調(diào)用unpark()許可證的個數(shù)也最多是1
即使我們先調(diào)用了unpark,再調(diào)用park也是可以正常喚醒線程的,因?yàn)閡npark獲取了一個許可證,之后再調(diào)用park方法,就可以名正言順的憑證消費(fèi),故不會阻塞。
思考:如果喚醒兩次后阻塞兩次,會是什么結(jié)果呢?
許可的數(shù)量最多為1,連續(xù)調(diào)用兩次unpark和調(diào)用一次unpark效果一樣,只會增加一個許可;而調(diào)用兩次park卻需要消費(fèi)兩個許可,證不夠,不能放行。線程就會阻塞
總結(jié)
- 調(diào)用park()時判斷許可是否大于0,如果大于0繼續(xù)運(yùn)行,如果不大于0線程就進(jìn)入阻塞,此時會再創(chuàng)建一個 ThreadBlockInVM 許可是否大于0,如果大于0就將許可設(shè)置為0,放行運(yùn)行
- 調(diào)用unpark()將許可設(shè)置為1,無論調(diào)用多少次都是1,如果許可為0,則還會調(diào)用 pthread_cond_signal喚醒在park中的線程
- 先調(diào)用unpark,再調(diào)用park,線程仍然可以被喚醒繼續(xù)執(zhí)行
根據(jù)合集中《Java線程通信》一文,我們將線程阻塞和喚醒的4種方案介紹完畢,如果在工作或者面試中碰到一定要想起來使用方法和細(xì)節(jié)
文章出自:石添的編程哲學(xué),如有轉(zhuǎn)載本文請聯(lián)系【石添的編程哲學(xué)】今日頭條號。