java线程主要状态及转换_Java线程状态转换及控制
線程的狀態(tài)(系統(tǒng)層面)
一個線程被創(chuàng)建后就進入了線程的生命周期。在線程的生命周期中,共包括新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)這五種狀態(tài)。當線程啟動以后,CPU需要在多個線程之間切換,所以線程也會隨之在運行、阻塞、就緒這幾種狀態(tài)之間切換。
線程的狀態(tài)轉換如圖:
當一個正在運行的線程遇到如下情況時,線程會從運行態(tài)轉為阻塞態(tài):
①?線程調用sleep、join等方法。
②?線程調用了一個阻塞式IO方法。
③?線程試圖獲得一個同步監(jiān)視器,但是該監(jiān)視器正在被其他線程持有。
④ 線程在等待某個 notify 通知。
⑤ 程序調用了線程的suspend方法將該線程掛起。
當線程被阻塞后,其他線程就有機會獲得CPU資源而被執(zhí)行。當上述導致線程被阻塞的因素解除后,線程會回到就緒狀態(tài)等待處理機調度而被執(zhí)行。
當一個線程執(zhí)行結束后,該線程進入死亡狀態(tài)。
有以下3種方式可結束一個線程:
① run 方法執(zhí)行完畢。
② 線程拋出一個異常或錯誤,而該異常或錯誤未被捕獲。
③ 調用線程的 stop方法結束該線程。(不推薦使用)
線程的控制
Thread類中提供了一些控制線程的方法,通過這些方法可以輕松地控制一個線程的執(zhí)行和運行狀態(tài),以達到程序的預期效果。
join 方法
如果線程A調用了線程B的join方法,線程A將被阻塞,等待線程B執(zhí)行完畢后線程A才會被執(zhí)行。這里需要注意一點的是,join方法必須在線程B的start方法調用之后調用才有意義。join方法的主要作用就是實現(xiàn)線程間的同步,它可以使線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。
join 方法有以下3種重載形式:
① join(): 等待被join的線程執(zhí)行完成。
② join(long millis): 等待被join 的線程的時間為 millis 毫秒,如果該線程在millis 毫秒內未結束,則不再等待。
③ join(long millis,int nanos): 等待被join的線程的時間最長為 millis 毫秒加上nanos微秒。
public class JoinThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class TestThreadState {
public static void main(String[] args) {
// 創(chuàng)建要加入當前線程的線程,并啟動
JoinThread j1 = new JoinThread();
j1.start();
// 加入當前線程,阻塞當前線程,直到加入線程執(zhí)行完畢
try {
j1.join();
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
我們定義了一個JoinThread類,它繼承了Thread類,這是我們要加入的線程類。
在main方法中,我們創(chuàng)建了JoinThread線程,并把它加入到當前線程(主線程)中,并沒有指定當前線程等待的時間,所以會一直阻塞當前線程,直到JoinThread線程的run方法執(zhí)行完畢,才會繼續(xù)執(zhí)行當前線程。
sleep 方法
當線程A調用了 sleep方法,則線程A將被阻塞,直到指定睡眠的時間到達后,線程A才會重新被喚起,進入就緒狀態(tài)。
sleep方法有以下2種重載形式:
① sleep(long millis):讓當前正在執(zhí)行的線程暫停millis毫秒,該線程進入阻塞狀態(tài)。
② sleep(long mills,long nanos):讓當前正在執(zhí)行的線程暫停 millis 毫秒加上 nanos微秒。
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
try {
Thread.sleep(1000); // 阻塞當前線程1s
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
這段代碼中沒有創(chuàng)建其他線程,只有當前線程存在,也就是執(zhí)行main函數(shù)的主線程。for循環(huán)中每打印一次線程名稱,主線程就會被sleep方法阻塞1s,然后進入就緒狀態(tài),重新等待被調到,實現(xiàn)了線程的控制。
yield 方法
當線程A調用了yield方法,它可以暫時放棄處理器,但是線程A不會被阻塞,而是進入就緒狀態(tài)。
public class YieldThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
// 主動放棄
Thread.yield();
}
}
}
我們自定義了一個線程類YieldThread,在run方法中定義了一個for循環(huán),for循環(huán)中每打印一次線程名稱,就會調用一下yield方法,主動放棄CUP讓給其它有相同優(yōu)先級或更高優(yōu)先級的線程,自己進入就緒狀態(tài),等待被CPU調度。
設置線程的優(yōu)先級
每個線程都有自己的優(yōu)先級,默認情況下線程的優(yōu)先級都與創(chuàng)建該線程的父線程的優(yōu)先級相回。同時Thread類提供了setPriority(int priority) 和getPriority()方法設置和返回指定線程的優(yōu)先級。參數(shù)priority是一個整型數(shù)據(jù),用以指定線程的優(yōu)先級。priority 的取值范圍是1-10,默認值為5,也可以使用Thread類提供的三個靜態(tài)常量設置線程的優(yōu)先級。
① MAX_PRIORITY:最高優(yōu)先級,其值為10。
② MIN_PRIORITY:最低優(yōu)先級,其值為1。
③ NORM_PRIORITY:普通優(yōu)先級,其值為5。
public class TestThreadPriority {
public static void main(String[] args) {
// 線程優(yōu)先級
ThreadPriority p1 = new ThreadPriority();
p1.setName("p1");
ThreadPriority p2 = new ThreadPriority();
p2.setName("p2");
ThreadPriority p3 = new ThreadPriority();
p3.setName("p3");
p1.setPriority(1);
p3.setPriority(10);
p1.start();
p2.start();
p3.start();
}
}
我們創(chuàng)建了三個線程p1、p2、p3,設置了p1的優(yōu)先級為1,p3的優(yōu)先級為10,并沒有設置p2的,所以p2的優(yōu)先級默認是5。優(yōu)先級越高,表示獲取cup的機會越多,注意此處說的是機會,所以高優(yōu)先級的線程并不是一定先于低優(yōu)先級的線程被CPU調度,只是機會更大而已。
sleep方法和wait方法的區(qū)別是什么?
sleep?方法是Thread類的一個靜態(tài)方法,其作用是使運行中的線程暫時停止指定的毫秒數(shù),從而該線程進入阻塞狀態(tài)并讓出處理器,將執(zhí)行的機會讓給其他線程。但是這個過程中監(jiān)控狀態(tài)始終保持,當sleep的時間到了之后線程會自動恢復。
wait 方法是Object類的方法,它是用來實現(xiàn)線程同步的。當調用某個對象的wait方法后,當前線程會被阻塞并釋放同步鎖,直到其他線程調用了該對象的?notify?方法或者?notifyAll?方法來喚醒該線程。所以?wait?方法和?notify(或notifyAll)應當成對出現(xiàn)以保證線程間的協(xié)調運行。
sleep方法和yield方法的區(qū)別是什么?
① sleep方法暫停當前線程后,會給其他線程執(zhí)行機會而不會考慮其他線程的優(yōu)先級。但是yield方法只會給優(yōu)先級相同或者優(yōu)先級更高的線程執(zhí)行機會。
② sleep方法執(zhí)行后線程會進入阻塞狀態(tài),而執(zhí)行了yield方法后,當前線程會進入就緒狀態(tài)。
③ 由于sleep方法的聲明拋出了 InterruptedException 異常,所以在調用sleep方法時需要catch 該異常或拋出該異常,而yield 方法沒有聲明拋出異常。
④ sleep 方法比yield 方法具有更好的可移植性。
補充一下sleep、yield、join和wait的差異:
①?sleep、join、yield時并不釋放對象鎖資源,在wait操作時會釋放對象資源,wait在被notify/notifyAll喚醒時,重新去搶奪獲取對象鎖資源。
②?sleep、join、yield可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制塊中使用。
③?調用wait會立即釋放鎖,進入等待隊列,但是notify()不會立刻釋放sycronized(obj)中的對象鎖,必須要等notify()所在線程執(zhí)行完sycronized(obj)同步塊中的所有代碼才會釋放這把鎖,然后供等待的線程來搶奪對象鎖。
Java中為什么不建議使用stop和suspend方法終止線程?
在Java中可以使用stop 方法停止一個線程,使該線程進入死亡狀態(tài)。但是使用這種方法結束一個線程是不安全的,在編寫程序時應當禁止使用這種方法。
之所以說stop方法是線程不安全的,是因為一旦調用了Thread.stop()方法,工作線程將拋出一個ThreadDeath的異常,這會導致run方法結束執(zhí)行,而且結束的點是不可控的,也就是說,它可能執(zhí)行到run方法的任何一個位置就突然終止了。同時它還會釋放掉該線程所持有的鎖,這樣其他因為請求該鎖對象而被阻塞的線程就會獲得鎖對象而繼續(xù)執(zhí)行下去。一般情況下,加鎖的目的是保護數(shù)據(jù)的一致性,然而如果在調用Thread.stop()后線程立即終止,那么被保護數(shù)據(jù)就有可能出現(xiàn)不一致的情況(數(shù)據(jù)的狀態(tài)不可預知)。同時,該線程所持有的鎖突然被釋放,其他線程獲得同步鎖后可以進入臨界區(qū)使用這些被破壞的數(shù)據(jù),這將有可能導致一些很奇怪的應用程序錯誤發(fā)生,而且這種錯誤非常難以debug.所以在這里再次重申,不要試圖用stop 方法結束一個線程。
suspend方法可以阻塞一個線程,然而該線程雖然被阻塞,但它仍然持有之前獲得的鎖,這樣其他任何線程都不能訪問相同鎖對象保護的資源,除非被阻塞的線程被重新恢復。如果此時只有一個線程能夠恢復這個被suspend的線程,但前提是先要訪問被該線程鎖定的臨界資源。這樣便產(chǎn)生了死鎖。所以在編寫程序時,應盡量避免使用suspend,如確實需要阻塞一個線程的運行,最好使用wait方法,這樣既可以阻塞掉當前正在執(zhí)行的線程,同時又使得該線程不至于陷入死鎖。
用一句話說就是:stop方法是線程不安全的,可能產(chǎn)生不可預料的結果;suspend方法可能導致死鎖。
如何終止一個線程?
在Java?中不推薦使用stop方法和suspend方法終止一個線程,因為那是不安全的,那么要怎樣終止一個線程呢?
方法一:使用退出標志
正常情況下,當Thread?或?Runnable?類的run方法執(zhí)行完畢后該線程即可結束,但是有些情況下run方法可能永遠都不會停止,例如,在服務端程序中使用線程監(jiān)聽客戶端請求,或者執(zhí)行其他需要循環(huán)處理的任務。這時如果希望有機會終止該線程,可將執(zhí)行的任務放在一個循環(huán)中(例如?while循環(huán)),并設置一個boolean型的循環(huán)結束的標志。如果想使?while?循環(huán)在某一特定條件下退出,就可以通過設置這個標志為true或false?來控制?while?循環(huán)是否退出。這樣將線程結束的控制邏輯與線程本身邏輯結合在一起,可以保證線程安全可控地結束。
讓我們來看一看案例:
public class TestQuitSign {
// 退出標志
public static volatile boolean quitFlag = false;
// 退出標志:針對運行時的線程
public static void main(String[] args) {
// 線程一:每隔一秒,打印一條信息,當quitFlag為true時結束run方法。
new Thread() {
public void run() {
System.out.println("thread start...");
while (!quitFlag) {
try {
Thread.sleep(1000);
} catch (Exception e) {
}
System.out.println("thread running...");
}
System.out.println("thread end...");
}
}.start();
// 線程二:等待三秒,設置quitFlag為true,終止線程一。
new Thread() {
public void run() {
try {
Thread.sleep(3000);
} catch (Exception e) {
// TODO: handle exception
}
quitFlag = true;
}
}.start();
}
}
在上面這段程序中的main方法里創(chuàng)建了兩個線程,第一個線程的run方法中有一個while循環(huán),該循環(huán)通過boolean型變量quitFlag控制其是否結束。因為變量quitFlag的初始值為false,所以如果不修改該變量,第一個線程中的run方法將不會停止,也就是說,第一個線程將永遠不會終止,并且每隔1s在屏幕上打印出一條字符串。第二個線程的作用是通過修改變量quitFlag來終止第一個線程。在第二個線程的run方法中首先將線程阻塞3s,然后將quitFlag置為true.因為變量quitFlag是同一進程中兩個線程共享的變量,所以可以通過修改quitFlag的值來控制第一個線程的執(zhí)行。當變量quitFlag被置為true,第一個線程的while循環(huán)就可以終止,所以run方法就能執(zhí)行完畢,從而安全退出第一個線程。
注意,boolean?型變量?quitFlag 被聲明為?volatile,volatile?會保證變量在一個線程中的每一步操作在另一個線程中都是可見的,所以這樣可以確保將?quitFlag 置為true?后可以安全退出第一個線程。
方法二:使用?interrupt方法
使用退出線程標志的方法終止一個線程存在一定的局限性,主要的限制就是這種方法只對運行中的線程起作用,如果該線程被阻塞(例如,調用了?Thread.join()方法或者Thread.sleep()方法等)而處于不可運行的狀態(tài)時,則退出線程標志的方法將不會起作用。
在這種情況下,可以使用Thread?提供的?interrupt()方法終止一個線程。因為該方法雖然不會中斷一個正在運行的線程,但是它可以使一個被阻塞的線程拋出一個中斷異常,從而使線程提前結束阻塞狀態(tài),然后通過catch塊捕獲該異常,從而安全地結束該線程。
我們來看看下面的例子:
public class TestInterrupt {
// Interrupt方法: 針對阻塞狀態(tài)的線程
public static void main(String[] args) throws InterruptedException{
// 創(chuàng)建線程
Thread thread = new Thread() {
public void run() {
System.out.println("thread start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) { // 捕獲中斷異常
e.printStackTrace();
}
System.out.println("thread end...");
}
};
// 啟動線程
thread.start();
// 主線程等待1秒,拋出一個中斷信號
Thread.sleep(1000);
thread.interrupt();
}
}
在上面這段程序中的main方法里創(chuàng)建了一個線程,在該線程的?run?方法中調用?sleep?函數(shù)將該線程阻塞10s.然后調用Thread?類的?start?方法啟動該線程,該線程剛剛被啟動就進入阻塞狀態(tài)。主線程等待1s后調用thread.interrupt()拋出一個中斷信號,在run方法中的catch會正常捕獲到這個中斷信號,這樣被阻塞的該線程就會提前退出阻塞狀態(tài),不需要等待10s線程thread 就會被提前終止。
上述方法主要針對當前線程調用了Thread.join()或者 Thread.sleep()等方法而被阻塞時終止該線程。如果一個線程被I/O阻塞,則無法通過thread.interrupt()拋出一個中斷信號而離開阻塞狀態(tài)。這時可推而廣之,觸發(fā)一個與當前I/O0阻塞相關的異常,使其退出I/O阻塞,然后通過catch 塊捕獲該異常,從而安全地結束該線程。
線程的狀態(tài)(JVM層面)
我們在上面討論的線程狀態(tài)是從操作系統(tǒng)層面來看的,這樣看比較直觀,也容易理解,也是一個線程在操作系統(tǒng)中真實狀態(tài)的體現(xiàn)。下面我們來看看Java 中線程的狀態(tài)及轉換。
Java 線程狀態(tài)
在Java中線程的狀態(tài)有6種,我們來看一看JDK 1.8幫助文檔中的說明:
我們可以看到幫助文檔中的最后一行,這些狀態(tài)是不反映任何操作系統(tǒng)線程狀態(tài)的JVM層面的狀態(tài)。我們來具體看一看這六種狀態(tài):
NEW:初始狀態(tài),線程被創(chuàng)建,但是還沒有調用 start 方法。
RUNNABLED:運行狀態(tài),JAVA 線程把操作系統(tǒng)中的就緒和運行兩種狀態(tài)統(tǒng)稱為“運行狀態(tài)”。
BLOCKED:阻塞狀態(tài),表示線程進入等待狀態(tài),也就是線程因為某種原因放棄了 CPU 使用權,阻塞也分為幾種情況 :
等待阻塞:運行的線程執(zhí)行了?Thread.sleep?、wait()、?join()?等方法JVM 會把當前線程設置為等待狀態(tài),當 sleep 結束、join 線程終止或者wait線程被喚醒后,該線程從等待狀態(tài)進入到阻塞狀態(tài),重新?lián)屨兼i后進行線程恢復;
同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被其他線程鎖占用了,那么jvm會把當前的線程放入到鎖池中 ;
其他阻塞:發(fā)出了 I/O請求時,JVM 會把當前線程設置為阻塞狀態(tài),當 I/O處理完畢則線程恢復;
WAITING:等待狀態(tài),沒有超時時間,要被其他線程喚醒或者有其它的中斷操作;
執(zhí)行 wait()
執(zhí)行 join()
執(zhí)行 LockSupport.park()
TIME_WAITING:超時等待狀態(tài),超時以后自動返回;
執(zhí)行 sleep(long)
執(zhí)行 wait(long)、join(long)
執(zhí)行 LockSupport.parkNanos(long)、LockSupport.parkUntil(long)
TERMINATED:終止狀態(tài),表示當前線程執(zhí)行完畢 。
Java 線程狀態(tài)轉換
總結
操作系統(tǒng)層面:
有5個狀態(tài),分別是:New(新建)、Runnable(就緒)、Running(運行)、Blocked(阻塞)、Dead(死亡)。
JVM層面:
有6個狀態(tài),分別是:NEW(新建)、RUNNABLE(運行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超時等待)、TERMINATED(終止)。
主要由這幾個方法來控制:sleep、join、yield、wait、notify以及notifyALL。
總結
以上是生活随笔為你收集整理的java线程主要状态及转换_Java线程状态转换及控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 带括号的计算器 java_【福利】jav
- 下一篇: java自动化初始变量_Java自动化测