☆啃碎并發(fā)(二):Java線程的生命周期

0 前言

當(dāng)線程被創(chuàng)建并啟動以后,它既不是一啟動就進(jìn)入了執(zhí)行狀態(tài),也不是一直處于執(zhí)行狀態(tài)。在線程的生命周期中,它要經(jīng)過 新建(New)、就緒(Runnable)、運(yùn)行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態(tài)。尤其是當(dāng)線程啟動以后,它不可能一直"霸占"著CPU獨(dú)自運(yùn)行,所以CPU需要在多條線程之間切換,于是 線程狀態(tài)也會多次在運(yùn)行、阻塞之間切換

線程狀態(tài)轉(zhuǎn)換關(guān)系

1 新建(New)狀態(tài)

當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個線程之后,該線程就處于 新建狀態(tài),此時的線程情況如下:

  1. 此時JVM為其分配內(nèi)存,并初始化其成員變量的值;
  2. 此時線程對象沒有表現(xiàn)出任何線程的動態(tài)特征,程序也不會執(zhí)行線程的線程執(zhí)行體;

2 就緒(Runnable)狀態(tài)

當(dāng)線程對象調(diào)用了start()方法之后,該線程處于 就緒狀態(tài)。此時的線程情況如下:

  1. 此時JVM會為其 創(chuàng)建方法調(diào)用棧和程序計數(shù)器;
  2. 該狀態(tài)的線程一直處于 線程就緒隊列(盡管是采用隊列形式,事實(shí)上,把它稱為可運(yùn)行池而不是可運(yùn)行隊列。因?yàn)镃PU的調(diào)度不一定是按照先進(jìn)先出的順序來調(diào)度的),線程并沒有開始運(yùn)行;
  3. 此時線程 等待系統(tǒng)為其分配CPU時間片,并不是說執(zhí)行了start()方法就立即執(zhí)行;

調(diào)用start()方法與run()方法,對比如下:

  1. 調(diào)用start()方法來啟動線程,系統(tǒng)會把該run()方法當(dāng)成線程執(zhí)行體來處理。但如果直接調(diào)用線程對象的run()方法,則run()方法立即就會被執(zhí)行,而且在run()方法返回之前其他線程無法并發(fā)執(zhí)行。也就是說,系統(tǒng)把線程對象當(dāng)成一個普通對象,而run()方法也是一個普通方法,而不是線程執(zhí)行體
  2. 需要指出的是,調(diào)用了線程的run()方法之后,該線程已經(jīng)不再處于新建狀態(tài),不要再次調(diào)用線程對象的start()方法。只能對處于新建狀態(tài)的線程調(diào)用start()方法,否則將引發(fā)IllegaIThreadStateExccption異常;

如何讓子線程調(diào)用start()方法之后立即執(zhí)行而非"等待執(zhí)行":

程序可以使用Thread.sleep(1) 來讓當(dāng)前運(yùn)行的線程(主線程)睡眠1毫秒,1毫秒就夠了,因?yàn)樵谶@1毫秒內(nèi)CPU不會空閑,它會去執(zhí)行另一個處于就緒狀態(tài)的線程,這樣就可以讓子線程立即開始執(zhí)行;

3 運(yùn)行(Running)狀態(tài)

當(dāng)CPU開始調(diào)度處于 就緒狀態(tài) 的線程時,此時線程獲得了CPU時間片才得以真正開始執(zhí)行run()方法的線程執(zhí)行體,則該線程處于 運(yùn)行狀態(tài)。

  1. 如果計算機(jī)只有一個CPU,那么在任何時刻只有一個線程處于運(yùn)行狀態(tài);
  2. 如果在一個多處理器的機(jī)器上,將會有多個線程并行執(zhí)行,處于運(yùn)行狀態(tài);
  3. 當(dāng)線程數(shù)大于處理器數(shù)時,依然會存在多個線程在同一個CPU上輪換的現(xiàn)象;

處于運(yùn)行狀態(tài)的線程最為復(fù)雜,它 不可能一直處于運(yùn)行狀態(tài)(除非它的線程執(zhí)行體足夠短,瞬間就執(zhí)行結(jié)束了),線程在運(yùn)行過程中需要被中斷,目的是使其他線程獲得執(zhí)行的機(jī)會,線程調(diào)度的細(xì)節(jié)取決于底層平臺所采用的策略。線程狀態(tài)可能會變?yōu)?阻塞狀態(tài)、就緒狀態(tài)和死亡狀態(tài)。比如:

  1. 對于采用 搶占式策略 的系統(tǒng)而言,系統(tǒng)會給每個可執(zhí)行的線程分配一個時間片來處理任務(wù);當(dāng)該時間片用完后,系統(tǒng)就會剝奪該線程所占用的資源,讓其他線程獲得執(zhí)行的機(jī)會。線程就會又 從運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài),重新等待系統(tǒng)分配資源;
  2. 對于采用 協(xié)作式策略的系統(tǒng)而言,只有當(dāng)一個線程調(diào)用了它的yield()方法后才會放棄所占用的資源—也就是必須由該線程主動放棄所占用的資源,線程就會又 從運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài)。

4 阻塞(Blocked)狀態(tài)

處于運(yùn)行狀態(tài)的線程在某些情況下,讓出CPU并暫時停止自己的運(yùn)行,進(jìn)入 阻塞狀態(tài)。

當(dāng)發(fā)生如下情況時,線程將會進(jìn)入阻塞狀態(tài):

  1. 線程調(diào)用sleep()方法,主動放棄所占用的處理器資源,暫時進(jìn)入中斷狀態(tài)(不會釋放持有的對象鎖),時間到后等待系統(tǒng)分配CPU繼續(xù)執(zhí)行;
  2. 線程調(diào)用一個阻塞式IO方法,在該方法返回之前,該線程被阻塞;
  3. 線程試圖獲得一個同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有;
  4. 程序調(diào)用了線程的suspend方法將線程掛起;
  5. 線程調(diào)用wait,等待notify/notifyAll喚醒時(會釋放持有的對象鎖);

阻塞狀態(tài)分類:

  1. 等待阻塞:運(yùn)行狀態(tài)中的 線程執(zhí)行wait()方法,使本線程進(jìn)入到等待阻塞狀態(tài);
  2. 同步阻塞:線程在 獲取synchronized同步鎖失敗(因?yàn)殒i被其它線程占用),它會進(jìn)入到同步阻塞狀態(tài);
  3. 其他阻塞:通過調(diào)用線程的 sleep()或join()或發(fā)出I/O請求 時,線程會進(jìn)入到阻塞狀態(tài)。當(dāng) sleep()狀態(tài)超時、join()等待線程終止或者超時、或者I/O處理完畢 時,線程重新轉(zhuǎn)入就緒狀態(tài);

在阻塞狀態(tài)的線程只能進(jìn)入就緒狀態(tài),無法直接進(jìn)入運(yùn)行狀態(tài)。而就緒和運(yùn)行狀態(tài)之間的轉(zhuǎn)換通常不受程序控制,而是由系統(tǒng)線程調(diào)度所決定。當(dāng)處于就緒狀態(tài)的線程獲得處理器資源時,該線程進(jìn)入運(yùn)行狀態(tài);當(dāng)處于運(yùn)行狀態(tài)的線程失去處理器資源時,該線程進(jìn)入就緒狀態(tài)

但有一個方法例外,調(diào)用yield()方法可以讓運(yùn)行狀態(tài)的線程轉(zhuǎn)入就緒狀態(tài)

4.1 等待(WAITING)狀態(tài)

線程處于 無限制等待狀態(tài),等待一個特殊的事件來重新喚醒,如:

  1. 通過wait()方法進(jìn)行等待的線程等待一個notify()或者notifyAll()方法;
  2. 通過join()方法進(jìn)行等待的線程等待目標(biāo)線程運(yùn)行結(jié)束而喚醒;

以上兩種一旦通過相關(guān)事件喚醒線程,線程就進(jìn)入了 就緒(RUNNABLE)狀態(tài) 繼續(xù)運(yùn)行。

4.2 時限等待(TIMED_WAITING)狀態(tài)

線程進(jìn)入了一個 時限等待狀態(tài),如:

sleep(3000),等待3秒后線程重新進(jìn)行 就緒(RUNNABLE)狀態(tài) 繼續(xù)運(yùn)行。

5 死亡(Dead)狀態(tài)

線程會以如下3種方式結(jié)束,結(jié)束后就處于 死亡狀態(tài)

  1. run()或call()方法執(zhí)行完成,線程正常結(jié)束;
  2. 線程拋出一個未捕獲的Exception或Error;
  3. 直接調(diào)用該線程stop()方法來結(jié)束該線程—該方法容易導(dǎo)致死鎖,通常不推薦使用;

處于死亡狀態(tài)的線程對象也許是活的,但是,它已經(jīng)不是一個單獨(dú)執(zhí)行的線程。線程一旦死亡,就不能復(fù)生。 如果在一個死去的線程上調(diào)用start()方法,會拋出java.lang.IllegalThreadStateException異常。

所以,需要注意的是:

一旦線程通過start()方法啟動后就再也不能回到新建(NEW)狀態(tài),線程終止后也不能再回到就緒(RUNNABLE)狀態(tài)

5.1 終止(TERMINATED)狀態(tài)

線程執(zhí)行完畢后,進(jìn)入終止(TERMINATED)狀態(tài)。

6 線程相關(guān)方法

public class Thread{
    // 線程的啟動
    public void start(); 
    // 線程體
    public void run(); 
    // 已廢棄
    public void stop(); 
    // 已廢棄
    public void resume(); 
    // 已廢棄
    public void suspend(); 
    // 在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠
    public static void sleep(long millis); 
    // 同上,增加了納秒?yún)?shù)
    public static void sleep(long millis, int nanos); 
    // 測試線程是否處于活動狀態(tài)
    public boolean isAlive(); 
    // 中斷線程
    public void interrupt(); 
    // 測試線程是否已經(jīng)中斷
    public boolean isInterrupted(); 
    // 測試當(dāng)前線程是否已經(jīng)中斷
    public static boolean interrupted(); 
    // 等待該線程終止
    public void join() throws InterruptedException; 
    // 等待該線程終止的時間最長為 millis 毫秒
    public void join(long millis) throws InterruptedException; 
    // 等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒
    public void join(long millis, int nanos) throws InterruptedException; 
}
線程方法狀態(tài)轉(zhuǎn)換

6.1 線程就緒、運(yùn)行和死亡狀態(tài)轉(zhuǎn)換

  1. 就緒狀態(tài)轉(zhuǎn)換為運(yùn)行狀態(tài):此線程得到CPU資源;
  2. 運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài):此線程主動調(diào)用yield()方法或在運(yùn)行過程中失去CPU資源。
  3. 運(yùn)行狀態(tài)轉(zhuǎn)換為死亡狀態(tài):此線程執(zhí)行執(zhí)行完畢或者發(fā)生了異常;

注意:

當(dāng)調(diào)用線程中的yield()方法時,線程從運(yùn)行狀態(tài)轉(zhuǎn)換為就緒狀態(tài),但接下來CPU調(diào)度就緒狀態(tài)中的那個線程具有一定的隨機(jī)性,因此,可能會出現(xiàn)A線程調(diào)用了yield()方法后,接下來CPU仍然調(diào)度了A線程的情況。

6.2 run & start

通過調(diào)用start啟動線程,線程執(zhí)行時會執(zhí)行run方法中的代碼。

  1. start():線程的啟動;
  2. run():線程的執(zhí)行體;

6.3 sleep & yield

sleep():通過sleep(millis)使線程進(jìn)入休眠一段時間,該方法在指定的時間內(nèi)無法被喚醒,同時也不會釋放對象鎖

比如,我們想要使主線程每休眠100毫秒,然后再打印出數(shù)字:

/**
 * 可以明顯看到打印的數(shù)字在時間上有些許的間隔
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        for(int i=0;i<100;i++){  
            System.out.println("main"+i);  
            Thread.sleep(100);  
        }  
    }  
} 

注意如下幾點(diǎn)問題:

  1. sleep是靜態(tài)方法,最好不要用Thread的實(shí)例對象調(diào)用它,因?yàn)樗叩氖冀K是當(dāng)前正在運(yùn)行的線程,而不是調(diào)用它的線程對象,它只對正在運(yùn)行狀態(tài)的線程對象有效??聪旅娴睦樱?
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            System.out.println(Thread.currentThread().getName());  
            MyThread myThread=new MyThread();  
            myThread.start();  
            // 這里sleep的就是main線程,而非myThread線程 
            myThread.sleep(1000); 
            Thread.sleep(10);  
            for(int i=0;i<100;i++){  
                System.out.println("main"+i);  
            }  
        }  
    }  
    
  2. Java線程調(diào)度是Java多線程的核心,只有良好的調(diào)度,才能充分發(fā)揮系統(tǒng)的性能,提高程序的執(zhí)行效率。但是不管程序員怎么編寫調(diào)度,只能最大限度的影響線程執(zhí)行的次序,而不能做到精準(zhǔn)控制。因?yàn)槭褂胹leep方法之后,線程是進(jìn)入阻塞狀態(tài)的,只有當(dāng)睡眠的時間結(jié)束,才會重新進(jìn)入到就緒狀態(tài),而就緒狀態(tài)進(jìn)入到運(yùn)行狀態(tài),是由系統(tǒng)控制的,我們不可能精準(zhǔn)的去干涉它,所以如果調(diào)用Thread.sleep(1000)使得線程睡眠1秒,可能結(jié)果會大于1秒。
    public class Test1 {  
        public static void main(String[] args) throws InterruptedException {  
            new MyThread().start();  
            new MyThread().start();  
        }  
    }  
      
    class MyThread extends Thread {  
        @Override  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
                System.out.println(this.getName()+"線程" + i + "次執(zhí)行!");  
                try {  
                    Thread.sleep(50);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    } 
    
    看某一次的運(yùn)行結(jié)果:可以發(fā)現(xiàn),線程0首先執(zhí)行,然后線程1執(zhí)行一次,又了執(zhí)行一次。發(fā)現(xiàn)并不是按照sleep的順序執(zhí)行的。
    Thread-0線程0次執(zhí)行!  
    Thread-1線程0次執(zhí)行!  
    Thread-1線程1次執(zhí)行!  
    Thread-0線程1次執(zhí)行!  
    Thread-0線程2次執(zhí)行!  
    Thread-1線程2次執(zhí)行!  
    

yield():與sleep類似,也是Thread類提供的一個靜態(tài)的方法,它也可以讓當(dāng)前正在執(zhí)行的線程暫停,讓出CPU資源給其他的線程。但是和sleep()方法不同的是,它不會進(jìn)入到阻塞狀態(tài),而是進(jìn)入到就緒狀態(tài)。yield()方法只是讓當(dāng)前線程暫停一下,重新進(jìn)入就緒線程池中,讓系統(tǒng)的線程調(diào)度器重新調(diào)度器重新調(diào)度一次,完全可能出現(xiàn)這樣的情況:當(dāng)某個線程調(diào)用yield()方法之后,線程調(diào)度器又將其調(diào)度出來重新進(jìn)入到運(yùn)行狀態(tài)執(zhí)行

實(shí)際上,當(dāng)某個線程調(diào)用了yield()方法暫停之后,優(yōu)先級與當(dāng)前線程相同,或者優(yōu)先級比當(dāng)前線程更高的就緒狀態(tài)的線程更有可能獲得執(zhí)行的機(jī)會,當(dāng)然,只是有可能,因?yàn)槲覀儾豢赡芫_的干涉cpu調(diào)度線程。

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("低級", 1).start();  
        new MyThread("中級", 5).start();  
        new MyThread("高級", 10).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name, int pro) {  
        super(name);// 設(shè)置線程的名稱  
        this.setPriority(pro);// 設(shè)置優(yōu)先級  
    }  
  
    @Override  
    public void run() {  
        for (int i = 0; i < 30; i++) {  
            System.out.println(this.getName() + "線程第" + i + "次執(zhí)行!");  
            if (i % 5 == 0)  
                Thread.yield();  
        }  
    }  
}  

關(guān)于sleep()方法和yield()方的區(qū)別如下

  1. sleep方法暫停當(dāng)前線程后,會進(jìn)入阻塞狀態(tài),只有當(dāng)睡眠時間到了,才會轉(zhuǎn)入就緒狀態(tài)。而yield方法調(diào)用后 ,是直接進(jìn)入就緒狀態(tài),所以有可能剛進(jìn)入就緒狀態(tài),又被調(diào)度到運(yùn)行狀態(tài);
  2. sleep方法聲明拋出了InterruptedException,所以調(diào)用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方法則沒有聲明拋出任務(wù)異常;
  3. sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制并發(fā)線程的執(zhí)行;

6.4 join

線程的合并的含義就是 將幾個并行線程的線程合并為一個單線程執(zhí)行,應(yīng)用場景是 當(dāng)一個線程必須等待另一個線程執(zhí)行完畢才能執(zhí)行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態(tài)方法。

join有3個重載的方法:

void join()    
    當(dāng)前線程等該加入該線程后面,等待該線程終止。    
void join(long millis)    
    當(dāng)前線程等待該線程終止的時間最長為 millis 毫秒。 如果在millis時間內(nèi),該線程沒有執(zhí)行完,那么當(dāng)前線程進(jìn)入就緒狀態(tài),重新等待cpu調(diào)度   
void join(long millis,int nanos)    
    等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒。如果在millis時間內(nèi),該線程沒有執(zhí)行完,那么當(dāng)前線程進(jìn)入就緒狀態(tài),重新等待cpu調(diào)度

例子代碼,如下

/**
 * 在主線程中調(diào)用thread.join(); 就是將主線程加入到thread子線程后面等待執(zhí)行。不過有時間限制,為1毫秒。
 */
public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread t=new MyThread();  
        t.start();  
        t.join(1);//將主線程加入到子線程后面,不過如果子線程在1毫秒時間內(nèi)沒執(zhí)行完,則主線程便不再等待它執(zhí)行完,進(jìn)入就緒狀態(tài),等待cpu調(diào)度  
        for(int i=0;i<30;i++){  
            System.out.println(Thread.currentThread().getName() + "線程第" + i + "次執(zhí)行!");  
        }  
    }  
}  
  
class MyThread extends Thread {  
    @Override  
    public void run() {  
        for (int i = 0; i < 1000; i++) {  
            System.out.println(this.getName() + "線程第" + i + "次執(zhí)行!");  
        }  
    }  
}  

在JDK中join方法的源碼,如下:

public final synchronized void join(long millis)    throws InterruptedException {  
    long base = System.currentTimeMillis();  
    long now = 0;  
  
    if (millis < 0) {  
        throw new IllegalArgumentException("timeout value is negative");  
    }  
          
    if (millis == 0) {  
        while (isAlive()) {  
           wait(0);  
        }  
    } else {  
        while (isAlive()) {  
            long delay = millis - now;  
            if (delay <= 0) {  
                break;  
            }  
            wait(delay);  
            now = System.currentTimeMillis() - base;  
        }  
    }  
}  

join方法實(shí)現(xiàn)是通過調(diào)用wait方法實(shí)現(xiàn)。當(dāng)main線程調(diào)用t.join時候,main線程會獲得線程對象t的鎖(wait 意味著拿到該對象的鎖),調(diào)用該對象的wait(等待時間),直到該對象喚醒main線程,比如退出后。這就意味著main 線程調(diào)用t.join時,必須能夠拿到線程t對象的鎖。

6.5 suspend & resume (已過時)

suspend-線程進(jìn)入阻塞狀態(tài),但不會釋放鎖。此方法已不推薦使用,因?yàn)橥綍r不會釋放鎖,會造成死鎖的問題

resume-使線程重新進(jìn)入可執(zhí)行狀態(tài)。

為什么 Thread.suspend 和 Thread.resume 被廢棄了?

Thread.suspend 天生容易引起死鎖。如果目標(biāo)線程掛起時在保護(hù)系統(tǒng)關(guān)鍵資源的監(jiān)視器上持有鎖,那么其他線程在目標(biāo)線程恢復(fù)之前都無法訪問這個資源。如果要恢復(fù)目標(biāo)線程的線程在調(diào)用 resume 之前試圖鎖定這個監(jiān)視器,死鎖就發(fā)生了。這種死鎖一般自身表現(xiàn)為“凍結(jié)( frozen )”進(jìn)程。

其他相關(guān)資料:

  1. https://blog.csdn.net/dlite/article/details/4212915

6.6 stop(已過時)

不推薦使用,且以后可能去除,因?yàn)樗话踩?/strong>。為什么 Thread.stop 被廢棄了?

因?yàn)槠涮焐遣话踩摹?strong>停止一個線程會導(dǎo)致其解鎖其上被鎖定的所有監(jiān)視器(監(jiān)視器以在棧頂產(chǎn)生ThreadDeath異常的方式被解鎖)。如果之前被這些監(jiān)視器保護(hù)的任何對象處于不一致狀態(tài),其它線程看到的這些對象就會處于不一致狀態(tài)。這種對象被稱為受損的 (damaged)。當(dāng)線程在受損的對象上進(jìn)行操作時,會導(dǎo)致任意行為。這種行為可能微妙且難以檢測,也可能會比較明顯。

不像其他未受檢的(unchecked)異常, ThreadDeath 悄無聲息的殺死及其他線程。因此,用戶得不到程序可能會崩潰的警告。崩潰會在真正破壞發(fā)生后的任意時刻顯現(xiàn),甚至在數(shù)小時或數(shù)天之后。

其他相關(guān)資料:

  1. https://blog.csdn.net/dlite/article/details/4212915

6.7 wait & notify/notifyAll

wait & notify/notifyAll這三個都是Object類的方法。使用 wait ,notify 和 notifyAll 前提是先獲得調(diào)用對象的鎖

  1. 調(diào)用 wait 方法后,釋放持有的對象鎖,線程狀態(tài)有 Running 變?yōu)?Waiting,并將當(dāng)前線程放置到對象的 等待隊列
  2. 調(diào)用notify 或者 notifyAll 方法后,等待線程依舊不會從 wait 返回,需要調(diào)用 noitfy 的線程釋放鎖之后,等待線程才有機(jī)會從 wait 返回;
  3. notify 方法:將等待隊列的一個等待線程從等待隊列種移到同步隊列中 ,而 notifyAll 方法:將等待隊列種所有的線程全部移到同步隊列,被移動的線程狀態(tài)由 Waiting 變?yōu)?Blocked。

前面一直提到兩個概念,等待隊列(等待池),同步隊列(鎖池),這兩者是不一樣的。具體如下:

同步隊列(鎖池):假設(shè)線程A已經(jīng)擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調(diào)用這個對象的某個synchronized方法(或者synchronized塊),由于這些線程在進(jìn)入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權(quán),但是該對象的鎖目前正被線程A擁有,所以這些線程就進(jìn)入了該對象的同步隊列(鎖池)中,這些線程狀態(tài)為Blocked。

等待隊列(等待池):假設(shè)一個線程A調(diào)用了某個對象的wait()方法,線程A就會釋放該對象的鎖(因?yàn)閣ait()方法必須出現(xiàn)在synchronized中,這樣自然在執(zhí)行wait()方法之前線程A就已經(jīng)擁有了該對象的鎖),同時 線程A就進(jìn)入到了該對象的等待隊列(等待池)中,此時線程A狀態(tài)為Waiting。如果另外的一個線程調(diào)用了相同對象的notifyAll()方法,那么 處于該對象的等待池中的線程就會全部進(jìn)入該對象的同步隊列(鎖池)中,準(zhǔn)備爭奪鎖的擁有權(quán)。如果另外的一個線程調(diào)用了相同對象的notify()方法,那么 僅僅有一個處于該對象的等待池中的線程(隨機(jī))會進(jìn)入該對象的同步隊列(鎖池)。

被notify或notifyAll喚起的線程是有規(guī)律的,具體如下:

  1. 如果是通過notify來喚起的線程,那 先進(jìn)入wait的線程會先被喚起來
  2. 如果是通過nootifyAll喚起的線程,默認(rèn)情況是 最后進(jìn)入的會先被喚起來,即LIFO的策略;

6.8 線程優(yōu)先級

每個線程執(zhí)行時都有一個優(yōu)先級的屬性,優(yōu)先級高的線程可以獲得較多的執(zhí)行機(jī)會,而優(yōu)先級低的線程則獲得較少的執(zhí)行機(jī)會。與線程休眠類似,線程的優(yōu)先級仍然無法保障線程的執(zhí)行次序。只不過,優(yōu)先級高的線程獲取CPU資源的概率較大,優(yōu)先級低的也并非沒機(jī)會執(zhí)行。

每個線程默認(rèn)的優(yōu)先級都與創(chuàng)建它的父線程具有相同的優(yōu)先級,在默認(rèn)情況下,main線程具有普通優(yōu)先級;

Thread類提供了setPriority(int newPriority)和getPriority()方法來設(shè)置和返回一個指定線程的優(yōu)先級,其中setPriority方法的參數(shù)是一個整數(shù),范圍是1~10之間,也可以使用Thread類提供的三個靜態(tài)常量:

MAX_PRIORITY   =10
MIN_PRIORITY   =1
NORM_PRIORITY   =5

例子代碼,如下

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        new MyThread("高級", 10).start();  
        new MyThread("低級", 1).start();  
    }  
}  
  
class MyThread extends Thread {  
    public MyThread(String name,int pro) {  
        super(name);//設(shè)置線程的名稱  
        setPriority(pro);//設(shè)置線程的優(yōu)先級  
    }  
    @Override  
    public void run() {  
        for (int i = 0; i < 100; i++) {  
            System.out.println(this.getName() + "線程第" + i + "次執(zhí)行!");  
        }  
    }  
}  

從執(zhí)行結(jié)果可以看到 ,一般情況下,高級線程更顯執(zhí)行完畢。

注意一點(diǎn)

雖然Java提供了10個優(yōu)先級別,但這些優(yōu)先級別需要操作系統(tǒng)的支持。不同的操作系統(tǒng)的優(yōu)先級并不相同,而且也不能很好的和Java的10個優(yōu)先級別對應(yīng)。所以我們應(yīng)該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態(tài)常量來設(shè)定優(yōu)先級,這樣才能保證程序最好的可移植性。

6.9 守護(hù)線程

守護(hù)線程與普通線程寫法上基本沒啥區(qū)別,調(diào)用線程對象的方法setDaemon(true),則可以將其設(shè)置為守護(hù)線程。

守護(hù)線程使用的情況較少,但并非無用,舉例來說,JVM的垃圾回收、內(nèi)存管理等線程都是守護(hù)線程。還有就是在做數(shù)據(jù)庫應(yīng)用時候,使用的數(shù)據(jù)庫連接池,連接池本身也包含著很多后臺線程,監(jiān)控連接個數(shù)、超時時間、狀態(tài)等等。

setDaemon方法詳細(xì)說明

public final void setDaemon(boolean on):將該線程標(biāo)記為守護(hù)線程或用戶線程。當(dāng)正在運(yùn)行的線程都是守護(hù)線程時,Java 虛擬機(jī)退出。

該方法必須在啟動線程前調(diào)用。 該方法首先調(diào)用該線程的 checkAccess 方法,且不帶任何參數(shù)。這可能拋出 SecurityException(在當(dāng)前線程中)。

參數(shù):

on - 如果為 true,則將該線程標(biāo)記為守護(hù)線程。

拋出:

 IllegalThreadStateException - 如果該線程處于活動狀態(tài)。
 SecurityException - 如果當(dāng)前線程無法修改該線程。
/** 
* Java線程:線程的調(diào)度-守護(hù)線程 
*/  
public class Test {  
        public static void main(String[] args) {  
                Thread t1 = new MyCommon();  
                Thread t2 = new Thread(new MyDaemon());  
                t2.setDaemon(true);        //設(shè)置為守護(hù)線程  
  
                t2.start();  
                t1.start();  
        }  
}  
  
class MyCommon extends Thread {  
        public void run() {  
                for (int i = 0; i < 5; i++) {  
                        System.out.println("線程1第" + i + "次執(zhí)行!");  
                        try {  
                                Thread.sleep(7);  
                        } catch (InterruptedException e) {  
                                e.printStackTrace();  
                        }  
                }  
        }  
}  
  
class MyDaemon implements Runnable {  
        public void run() {  
                for (long i = 0; i < 9999999L; i++) {  
                        System.out.println("后臺線程第" + i + "次執(zhí)行!");  
                        try {  
                                Thread.sleep(7);  
                        } catch (InterruptedException e) {  
                                e.printStackTrace();  
                        }  
                }  
        }  
}  

執(zhí)行結(jié)果:

后臺線程第0次執(zhí)行!  
線程1第0次執(zhí)行!  
線程1第1次執(zhí)行!  
后臺線程第1次執(zhí)行!  
后臺線程第2次執(zhí)行!  
線程1第2次執(zhí)行!  
線程1第3次執(zhí)行!  
后臺線程第3次執(zhí)行!  
線程1第4次執(zhí)行!  
后臺線程第4次執(zhí)行!  
后臺線程第5次執(zhí)行!  
后臺線程第6次執(zhí)行!  
后臺線程第7次執(zhí)行! 

從上面的執(zhí)行結(jié)果可以看出:前臺線程是保證執(zhí)行完畢的,后臺線程還沒有執(zhí)行完畢就退出了

實(shí)際上:JRE判斷程序是否執(zhí)行結(jié)束的標(biāo)準(zhǔn)是所有的前臺執(zhí)線程行完畢了,而不管后臺線程的狀態(tài),因此,在使用后臺線程時候一定要注意這個問題

6.10 如何結(jié)束一個線程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit 這些終止線程運(yùn)行的方法已經(jīng)被廢棄了,使用它們是極端不安全的!想要安全有效的結(jié)束一個線程,可以使用下面的方法。

  1. 正常執(zhí)行完run方法,然后結(jié)束掉;
  2. 控制循環(huán)條件和判斷條件的標(biāo)識符來結(jié)束掉線程;

比如run方法這樣寫:只要保證在一定的情況下,run方法能夠執(zhí)行完畢即可。而不是while(true)的無限循環(huán)。

class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                break;  
            i++;  
            System.out.println(i);  
              
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    boolean next=true;  
    @Override  
    public void run() {  
        while (next) {  
            if(i==10)  
                next=false;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  
或者
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            if(i==10)  
                return;  
            i++;  
            System.out.println(i);  
        }  
    }  
}  

誠然,使用上面方法的標(biāo)識符來結(jié)束一個線程,是一個不錯的方法,但其也有弊端,如果 該線程是處于sleep、wait、join的狀態(tài)時候,while循環(huán)就不會執(zhí)行,那么我們的標(biāo)識符就無用武之地了,當(dāng)然也不能再通過它來結(jié)束處于這3種狀態(tài)的線程了。

所以,此時可以使用interrupt這個巧妙的方式結(jié)束掉這個線程。我們先來看看sleep、wait、join方法的聲明:

public final void wait() throws InterruptedException 
public static native void sleep(long millis) throws InterruptedException
public final void join() throws InterruptedException

可以看到,這三者有一個共同點(diǎn),都拋出了一個InterruptedException的異常。在什么時候會產(chǎn)生這樣一個異常呢

每個Thread都有一個中斷狀狀態(tài),默認(rèn)為false??梢酝ㄟ^Thread對象的isInterrupted()方法來判斷該線程的中斷狀態(tài)??梢酝ㄟ^Thread對象的interrupt()方法將中斷狀態(tài)設(shè)置為true。

當(dāng)一個線程處于sleep、wait、join這三種狀態(tài)之一的時候,如果此時他的中斷狀態(tài)為true,那么它就會拋出一個InterruptedException的異常,并將中斷狀態(tài)重新設(shè)置為false。

看下面的簡單的例子:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
    }  
}  
  
class MyThread extends Thread {  
    int i=1;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            System.out.println(this.isInterrupted());  
            try {  
                System.out.println("我馬上去sleep了");  
                Thread.sleep(2000);  
                this.interrupt();  
            } catch (InterruptedException e) {  
                System.out.println("異常捕獲了"+this.isInterrupted());  
                return;  
            }  
            i++;  
        }  
    }  
}  

測試結(jié)果:

1  
false  
我馬上去sleep了  
2  
true  
我馬上去sleep了  
異常捕獲了false 

可以看到,首先執(zhí)行第一次while循環(huán),在第一次循環(huán)中,睡眠2秒,然后將中斷狀態(tài)設(shè)置為true。當(dāng)進(jìn)入到第二次循環(huán)的時候,中斷狀態(tài)就是第一次設(shè)置的true,當(dāng)它再次進(jìn)入sleep的時候,馬上就拋出了InterruptedException異常,然后被我們捕獲了。然后中斷狀態(tài)又被重新自動設(shè)置為false了(從最后一條輸出可以看出來)。

所以,我們可以使用interrupt方法結(jié)束一個線程。具體使用如下:

public class Test1 {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread thread=new MyThread();  
        thread.start();  
        Thread.sleep(3000);  
        thread.interrupt();  
    }  
}  
  
class MyThread extends Thread {  
    int i=0;  
    @Override  
    public void run() {  
        while (true) {  
            System.out.println(i);  
            try {  
                Thread.sleep(1000);  
            } catch (InterruptedException e) {  
                System.out.println("中斷異常被捕獲了");  
                return;  
            }  
            i++;  
        }  
    }  
} 

多測試幾次,會發(fā)現(xiàn)一般有兩種執(zhí)行結(jié)果:

0  
1  
2  
中斷異常被捕獲了

或者

0  
1  
2  
3  
中斷異常被捕獲了 

這兩種結(jié)果恰恰說明了,只要一個線程的中斷狀態(tài)一旦為true,只要它進(jìn)入sleep等狀態(tài),或者處于sleep狀態(tài),立馬回拋出InterruptedException異常

第一種情況,是當(dāng)主線程從3秒睡眠狀態(tài)醒來之后,調(diào)用了子線程的interrupt方法,此時子線程正處于sleep狀態(tài),立馬拋出InterruptedException異常。

第二種情況,是當(dāng)主線程從3秒睡眠狀態(tài)醒來之后,調(diào)用了子線程的interrupt方法,此時子線程還沒有處于sleep狀態(tài)。然后再第3次while循環(huán)的時候,在此進(jìn)入sleep狀態(tài),立馬拋出InterruptedException異常。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 來源: https://www.cnblogs.com/albertrui/p/8383799.html 一、前言...
    青青子衿zq閱讀 605評論 0 0
  • 本文主要講了java中多線程的使用方法、線程同步、線程數(shù)據(jù)傳遞、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等。 首先講...
    李欣陽閱讀 2,606評論 1 15
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 3,117評論 1 18
  • 林炳文Evankaka原創(chuàng)作品。轉(zhuǎn)載自http://blog.csdn.net/evankaka 本文主要講了ja...
    ccq_inori閱讀 741評論 0 4
  • 一擴(kuò)展javalangThread類二實(shí)現(xiàn)javalangRunnable接口三Thread和Runnable的區(qū)...
    和帥_db6a閱讀 597評論 0 1

友情鏈接更多精彩內(nèi)容