Java的線程阻塞、中斷及優(yōu)雅退出方法詳解
線程阻塞
一個線程進(jìn)入阻塞狀態(tài)的原因可能如下(已排除Deprecated方法):
sleep()
sleep()使當(dāng)前線程進(jìn)入停滯狀態(tài)(阻塞當(dāng)前線程),讓出CUP的使用、目的是不讓當(dāng)前線程獨自霸占該進(jìn)程所獲的CPU資源,以留一定時間給其他線程執(zhí)行的機(jī)會;
當(dāng)在一個Synchronized塊中調(diào)用Sleep()方法是,線程雖然休眠了,但是對象鎖并沒有被釋放,其他線程無法訪問這個對象(即使睡著也持有對象鎖)。
wait()
調(diào)用wait()/1.5中的condition.await()使線程掛起,直到線程獲取notify()/notifyAll()消息,(或者在Java SE5中java.util.concurrent類庫中等價的signal()/signalAll()消息),線程才會進(jìn)入就緒狀態(tài);
wait()調(diào)用會釋放當(dāng)前對象鎖(monitor),這樣其他線程可以繼續(xù)進(jìn)入對象的同步方法。
另外,調(diào)用join()也會導(dǎo)致線程阻塞,因為源碼中join()就是通過wait()實現(xiàn)的;
等待I/O;
class Demo3 implements Runnable throws InterruptedException{ private InputStream in; public void run(){ in.read(); } }
無法持有鎖進(jìn)入同步代碼
進(jìn)入同步代碼前無法獲取鎖,比如試圖調(diào)用synchronized方法,或者顯示鎖對象的上鎖行為ReentrantLock.lock(),而對應(yīng)鎖已被其他線程獲取的情況下都將導(dǎo)致線程進(jìn)入阻塞狀態(tài);
注意:yield()并不會導(dǎo)致線程轉(zhuǎn)到等待/睡眠/阻塞狀態(tài)。在大多數(shù)情況下,yield()將導(dǎo)致線程從運行狀態(tài)轉(zhuǎn)到可運行狀態(tài),但有可能沒有效果。
線程中斷
線程中斷可以在線程內(nèi)部設(shè)置一個中斷標(biāo)識,同時讓處于(可中斷)阻塞的線程拋出InterruptedException中斷異常,使線程跳出阻塞狀態(tài)。相比其他語言,Java線程中斷比較特殊,經(jīng)常會引起開發(fā)人員的誤解。因為中斷聽起來高深復(fù)雜,實質(zhì)原理上非常簡單。
中斷原理
Java中斷機(jī)制是一種協(xié)作機(jī)制,也就是說通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己處理中斷。這好比是家里的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎么注意身體則完全取決于自己。
Java中斷模型也是這么簡單,每個線程對象里都有一個boolean類型的標(biāo)識(不一定就要是Thread類的字段,實際上也的確不是,這幾個方法最終都是通過native方法來完成的),代表著是否有中斷請求(該請求可以來自所有線程,包括被中斷的線程本身)。例如,當(dāng)線程t1想中斷線程t2,只需要在線程t1中將線程t2對象的中斷標(biāo)識置為true,然后線程2可以選擇在合適的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一樣。
中斷相關(guān)的方法
方法 | 解釋 |
public static boolean interrupted() | 測試當(dāng)前線程是否已經(jīng)中斷。線程的中斷狀態(tài) 由該方法清除。換句話說,如果連續(xù)兩次調(diào)用該方法,則第二次調(diào)用將返回 false(在第一次調(diào)用已清除了其中斷狀態(tài)之后,且第二次調(diào)用檢驗完中斷狀態(tài)前,當(dāng)前線程再次中斷的情況除外)。 |
public boolean isInterrupted() | 測試線程是否已經(jīng)中斷。線程的中斷狀態(tài)不受該方法的影響。 |
public void interrupt() | 中斷線程,設(shè)置中斷標(biāo)識為為true。 |
其中,interrupt方法是唯一能將中斷狀態(tài)設(shè)置為true的方法。靜態(tài)方法interrupted會將當(dāng)前線程的中斷狀態(tài)清除,但這個方法的命名極不直觀,很容易造成誤解,需要特別注意。
此外,類庫中的有些類的方法也可能會調(diào)用中斷,如FutureTask中的cancel方法,如果傳入的參數(shù)為true,它將會在正在運行異步任務(wù)的線程上調(diào)用interrupt方法,如果正在執(zhí)行的異步任務(wù)中的代碼沒有對中斷做出響應(yīng),那么cancel方法中的參數(shù)將不會起到什么效果;
ExecutorService exec = Executors.newCachedThreadPool(); Futrue<?> f = exec.submit(new TaskThread()); f.interrupt();
又如ThreadPoolExecutor中的shutdownNow方法會遍歷線程池中的工作線程并調(diào)用線程的interrupt方法來中斷線程,所以如果工作線程中正在執(zhí)行的任務(wù)沒有對中斷做出響應(yīng),任務(wù)將一直執(zhí)行直到正常結(jié)束。
ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0;i<5;i++) exec.execute(new TaskThread()) exec.shutdownNow();
中斷的處理
中斷一個線程只是為了引起該線程的注意,被中斷線程可以決定如何應(yīng)對中斷。某些線程非常重要,以至于它們應(yīng)該不理會中斷,而是在處理完拋出的異常之后繼續(xù)執(zhí)行,但是更普遍的情況是,一個線程將把中斷看作一個終止請求,這種線程的run方法遵循如下形式:
public void run() { try { ... /* * 不管循環(huán)里是否調(diào)用過線程阻塞的方法如sleep、join、wait,這里還是需要加上 * !Thread.currentThread().isInterrupted()條件,雖然拋出異常后退出了循環(huán),顯 * 得用阻塞的情況下是多余的,但如果調(diào)用了阻塞方法但沒有阻塞時,這樣會更安全、更及時。 */ while (!Thread.currentThread().isInterrupted()&& more work to do) { do more work } } catch (InterruptedException e) { //線程在wait或sleep期間被中斷了 } finally { //線程結(jié)束前做一些清理工作 } }
上面是while循環(huán)在try塊里,如果try在while循環(huán)里時,因該在catch塊里重新設(shè)置一下中斷標(biāo)示,因為拋出InterruptedException異常后,中斷標(biāo)示位會自動清除,此時應(yīng)該這樣:
public void run() { while (!Thread.currentThread().isInterrupted()&& more work to do) { try { ... sleep(delay); //wait(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); //重新設(shè)置中斷標(biāo)示 } } }
可中斷阻塞
對于處于sleep,join等操作的線程,如果被調(diào)用interrupt()后,會拋出InterruptedException,然后線程的中斷標(biāo)志位會由true重置為false,因為線程為了處理異常已經(jīng)重新處于就緒狀態(tài)。
不可中斷的操作,包括進(jìn)入synchronized段以及Lock.lock(),inputSteam.read()等,調(diào)用interrupt()對于這幾個問題無效,因為它們都不拋出中斷異常。如果拿不到資源,它們會無限期阻塞下去。
對于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中斷的加鎖操作,它可以拋出中斷異常。等同于等待時間無限長的Lock.tryLock(long time, TimeUnit unit)。
對于inputStream等資源,有些(實現(xiàn)了interruptibleChannel接口)可以通過close()方法將資源關(guān)閉,對應(yīng)的阻塞也會被放開。
但是,你可能正使用Java1.0之前就存在的傳統(tǒng)的I/O,Thread.interrupt()將不起作用,因為線程將不會退出被阻塞狀態(tài)。
很幸運,Java平臺為這種情形提供了一項解決方案,即調(diào)用阻塞該線程的套接字的close()方法。在這種情形下,如果線程被I/O操作阻塞,當(dāng)調(diào)用該套接字的close方法時,該線程在調(diào)用accept地方法將接收到一個SocketException(SocketException為IOException的子異常)異常,這與使用interrupt()方法引起一個InterruptedException異常被拋出非常相似。
java.nio類庫提供了更加人性化的I/O中斷,被阻塞的nio通道會自動地響應(yīng)中斷,不需要關(guān)閉底層資源;
線程優(yōu)雅退出
一般情況下,線程退出可以使用while循環(huán)判斷共享變量條件的方式,當(dāng)線程內(nèi)有阻塞操作時,可能導(dǎo)致線程無法運行到條件判斷的地方而導(dǎo)致一直阻塞下去,這個時候就需要中斷來幫助線程脫離阻塞。因此比較優(yōu)雅的退出線程方式是結(jié)合共享變量和中斷。
thread = new Thread(new Runnable() { @Override public void run() { /* * 在這里為一個循環(huán),條件是判斷線程的中斷標(biāo)志位是否中斷 */ while (flag&&(!Thread.currentThread().isInterrupted())) { try { Log.i("tag","線程運行中"+Thread.currentThread().getId()); // 每執(zhí)行一次暫停40毫秒 //當(dāng)sleep方法拋出InterruptedException 中斷狀態(tài)也會被清掉 Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); //如果拋出異常則再次設(shè)置中斷請求 Thread.currentThread().interrupt(); } } } }); thread.start();
到此這篇關(guān)于Java的線程阻塞、中斷及優(yōu)雅退出方法詳解的文章就介紹到這了,更多相關(guān)Java線程阻塞、中斷及優(yōu)雅退出內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot項目整合注冊功能模塊開發(fā)實戰(zhàn)
這篇文章主要介紹了springboot項目整合注冊功能模塊開發(fā)實戰(zhàn),在用戶的注冊是首先需要查詢當(dāng)前的用戶名是否存在,如果存在則不能進(jìn)行注冊,相當(dāng)于一個查詢語句,本文通過實例代碼詳細(xì)講解,需要的朋友可以參考下2022-11-11使用jd-gui反編譯修改jar包里的.class并重新生成新jar問題
這篇文章主要介紹了使用jd-gui反編譯修改jar包里的.class并重新生成新jar問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05Java for each實現(xiàn)機(jī)制代碼原理解析
這篇文章主要介紹了Java for each實現(xiàn)機(jī)制代碼原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06SpringBoot?AOP?Redis實現(xiàn)延時雙刪功能實戰(zhàn)
本文主要介紹了SpringBoot?AOP?Redis實現(xiàn)延時雙刪功能實戰(zhàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08SpringBoot2底層注解@Configuration配置類詳解
這篇文章主要為大家介紹了SpringBoot2底層注解@Configuration配置類詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05