Java多線程之線程狀態(tài)和關(guān)鍵字講解

1 線程生命周期狀態(tài)

1.1 進(jìn)程和線程概念

現(xiàn)在的操作系統(tǒng)是多任務(wù)操作系統(tǒng)。多線程是實現(xiàn)多任務(wù)的一種方式
Java編寫程序都運(yùn)行在在Java虛擬機(jī)(JVM)中,在JVM的內(nèi)部,程序的多任務(wù)是通過線程來實現(xiàn)的。每用java命 令啟動一個java應(yīng)用程序,就會啟動一個JVM進(jìn)程。在同一個JVM進(jìn)程中,有且只有一個進(jìn)程,就是它自己。在這個JVM環(huán)境中,所有程序代碼的運(yùn)行都是以線程來運(yùn)行

Java程序中,JVM負(fù)責(zé)線程的調(diào)度。線程調(diào)度是值按照特定的機(jī)制為多個線程分配CPU的使用權(quán)。
調(diào)度的模式有兩種:分時調(diào)度搶占式調(diào)度。分時調(diào)度是所有線程輪流獲得CPU使用權(quán),并平均分配每個線程占用CPU的時間;搶占式調(diào)度是根據(jù)線程的優(yōu)先級別來獲取CPU的使用權(quán)。JVM的線程調(diào)度模式采用了搶占式模式

  • 進(jìn)程
    是指一個內(nèi)存中運(yùn)行的應(yīng)用程序,每個進(jìn)程都有自己獨(dú)立的一塊內(nèi)存空間,一個進(jìn)程中可以啟動多個線程,進(jìn)程是系統(tǒng)資源分配和調(diào)度的一個獨(dú)立單位
    比如在Windows系統(tǒng)中,一個運(yùn)行的exe就是一個進(jìn)程
  • 線程
    是指進(jìn)程中的一個執(zhí)行流程,一個進(jìn)程中可以運(yùn)行多個線程,是cpu調(diào)度和分配的基本單位,它比進(jìn)程更小,線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計數(shù)器,一組寄存器和棧),可以與同屬于一個進(jìn)程的其他線程共享進(jìn)程資源。比如java.exe進(jìn)程中可以運(yùn)行很多線程。線程總是屬于某個進(jìn)程,進(jìn)程中的多個線程共享進(jìn)程的內(nèi)存。
  • 進(jìn)程和線程的關(guān)系
    一個線程只能屬于一個進(jìn)程,而一個進(jìn)程可以有多個線程,但至少有一個線程。線程是操作系統(tǒng)可識別的最小執(zhí)行和調(diào)度單位。
    資源分配給進(jìn)程,同一進(jìn)程的所有線程共享該進(jìn)程的所有資源。 同一進(jìn)程中的多個線程共享代碼段(代碼和常量),數(shù)據(jù)段(全局變量和靜態(tài)變量),擴(kuò)展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運(yùn)行時段,用來存放所有局部變量和臨時變量。
    處理機(jī)分給線程,即真正在處理機(jī)上運(yùn)行的是線程。
    線程在執(zhí)行過程中,需要協(xié)作同步。不同進(jìn)程的線程間要利用消息通信的辦法實現(xiàn)同步。

1.2 線程生命狀態(tài)

線程生命狀態(tài)圖示:

image

線程生命狀態(tài)

  1. 新建狀態(tài)
    new關(guān)鍵字和Thread類或其子類建立一個線程對象后,該線程對象就處于新生狀態(tài)。處于新生狀態(tài)的線程有自己的內(nèi)存空間,通過調(diào)用start方法進(jìn)入就緒狀態(tài)(runnable)。
    注意:不能對已經(jīng)啟動的線程再次調(diào)用start()方法,否則會出現(xiàn)java.lang.IllegalThreadStateException異常。

  2. 就緒狀態(tài)
    處于就緒狀態(tài)的線程已經(jīng)具備了運(yùn)行條件,但還沒有分配到CPU,處于線程就緒隊列(盡管是采用隊列形式,事實上,把它稱為可運(yùn)行池而不是可運(yùn)行隊列,因為cpu的調(diào)度不一定是按照先進(jìn)先出的順序來調(diào)度的,每個支持多線程的系統(tǒng)都有一個排程器,排程器會從線程池中選擇一個線程并啟動它),等待系統(tǒng)為其分配CPU。等待狀態(tài)并不是執(zhí)行狀態(tài),當(dāng)系統(tǒng)選定一個等待執(zhí)行的Thread對象后,它就會從等待執(zhí)行狀態(tài)進(jìn)入執(zhí)行狀態(tài),系統(tǒng)挑選的動作稱之為cpu調(diào)度。一旦獲得CPU,線程就進(jìn)入運(yùn)行狀態(tài)并自動調(diào)用自己的run方法。
    提示:如果希望子線程調(diào)用start()方法后立即執(zhí)行,可以使用Thread.sleep()方式使主線程睡眠一伙兒,轉(zhuǎn)去執(zhí)行子線程。

  3. 運(yùn)行狀態(tài)
    處于運(yùn)行狀態(tài)的線程最為復(fù)雜,它可以變?yōu)?code>阻塞狀態(tài)、就緒狀態(tài)死亡狀態(tài)。
    處于就緒狀態(tài)的線程,如果獲得了cpu的調(diào)度,就會從就緒狀態(tài)變?yōu)檫\(yùn)行狀態(tài),執(zhí)行run()方法中的任務(wù)。如果該線程失去了cpu資源,就會又從運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài)。重新等待系統(tǒng)分配資源。也可以對在運(yùn)行狀態(tài)的線程調(diào)用yield()方法,它就會讓出cpu資源,再次變?yōu)榫途w狀態(tài)
    當(dāng)發(fā)生如下情況是,線程會從運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài):
    ①線程調(diào)用sleep方法主動放棄所占用的系統(tǒng)資源
    ②線程調(diào)用一個阻塞式IO方法,在該方法返回之前,該線程被阻塞
    ③線程試圖獲得一個同步監(jiān)視器,但更改同步監(jiān)視器正被其他線程所持有
    ④線程在等待某個通知(notify
    ⑤程序調(diào)用了線程的suspend方法將線程掛起。不過該方法容易導(dǎo)致死鎖,所以程序應(yīng)該盡量避免使用該方法。

  4. 阻塞狀態(tài)
    處于運(yùn)行狀態(tài)的線程在某些情況下,如執(zhí)行了sleep(睡眠)方法,或等待I/O設(shè)備等資源,將讓出CPU并暫時停止自己的運(yùn)行,進(jìn)入阻塞狀態(tài)。
    在阻塞狀態(tài)的線程不能進(jìn)入就緒隊列。只有當(dāng)引起阻塞的原因消除時,如睡眠時間已到,或等待的I/O設(shè)備空閑下來,線程便轉(zhuǎn)入就緒狀態(tài),重新到就緒隊列中排隊等待,被系統(tǒng)選中后 從原來停止的位置開始繼續(xù)運(yùn)行
    阻塞的情況分三種:
    等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會把該線程放入等待池中。
    同步阻塞:運(yùn)行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
    其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請求時,JVM會把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉(zhuǎn)入就緒狀態(tài)

  5. 死亡狀態(tài)
    當(dāng)線程的run()方法執(zhí)行完,或者被強(qiáng)制性地終止,就認(rèn)為它死去。這個線程對象也許是活的,但是,它已經(jīng)不是一個單獨(dú)執(zhí)行的線程。線程一旦死亡, 就不能復(fù)生。 如果在一個死去的線程上調(diào)用start()方法,會拋出java.lang.IllegalThreadStateException異常

2 關(guān)鍵字講解

2.1 線程合并join

線程的合并的含義就是將幾個并行線程的線程合并為一個單線程執(zhí)行,應(yīng)用場景是當(dāng)一個線程必須等待另一個線程執(zhí)行完畢才能執(zhí)行時可以使用join方法。
join為非靜態(tài)方法,定義如下:

  • void join():等待該線程終止
  • void join(long millis):等待該線程終止的時間最長為 millis 毫秒。
  • void join(long millis, int nanos):等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒。
public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                t1.start(); 

                for (int i = 0; i < 20; i++) { 
                        System.out.println("主線程第" + i + "次執(zhí)行!"); 
                        if (i > 2) try { 
                                //t1線程合并到主線程中,主線程停止執(zhí)行過程,轉(zhuǎn)而執(zhí)行t1線程,直到t1執(zhí)行完畢后繼續(xù)。 
                                t1.join(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("線程1第" + i + "次執(zhí)行!"); 
                } 
        } 
}

2.2 線程讓步y(tǒng)ield

線程的讓步含義就是使當(dāng)前運(yùn)行著線程讓出CPU資源,但是然給誰不知道,僅僅是讓出,線程狀態(tài)回到可運(yùn)行狀態(tài)。

線程的讓步使用Thread.yield()方法,yield() 為靜態(tài)方法,功能是暫停當(dāng)前正在執(zhí)行的線程對象,并執(zhí)行其他線程。 但是yield喚醒的是相同優(yōu)先級或者更高優(yōu)先級的線程

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 

                t2.start(); 
                t1.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("線程1第" + i + "次執(zhí)行!"); 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("線程2第" + i + "次執(zhí)行!"); 
                        Thread.yield(); 
                } 
        } 
}
 
線程2第0次執(zhí)行! 
線程2第1次執(zhí)行! 
線程2第2次執(zhí)行! 
線程2第3次執(zhí)行! 
線程1第0次執(zhí)行! 
線程1第1次執(zhí)行! 
線程1第2次執(zhí)行! 
線程1第3次執(zhí)行! 
線程1第4次執(zhí)行! 
線程1第5次執(zhí)行! 
線程1第6次執(zhí)行! 
線程1第7次執(zhí)行! 
線程1第8次執(zhí)行! 
線程1第9次執(zhí)行! 
線程2第4次執(zhí)行! 
線程2第5次執(zhí)行! 
線程2第6次執(zhí)行! 
線程2第7次執(zhí)行! 
線程2第8次執(zhí)行! 
線程2第9次執(zhí)行! 

2.3 線程休眠sleep

線程休眠的目的是使線程讓出CPU的最簡單的做法之一,線程休眠時候,會將CPU資源交給其他線程,以便能輪換執(zhí)行,當(dāng)休眠一定時間后,線程會蘇醒,進(jìn)入準(zhǔn)備狀態(tài)等待執(zhí)行。

線程休眠的方法是Thread.sleep(long millis)和Thread.sleep(long millis, int nanos),均為靜態(tài)方法,那調(diào)用sleep休眠的哪個線程呢,簡單說,哪個線程調(diào)用sleep,就休眠哪個線程
sleep方法本身會給其他線程一個運(yùn)行的機(jī)會 但是不考慮優(yōu)先級,因此會給優(yōu)先級較低的線程一個機(jī)會

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.start(); 
                t2.start(); 
        } 
} 

class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("線程1第" + i + "次執(zhí)行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 

class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 3; i++) { 
                        System.out.println("線程2第" + i + "次執(zhí)行!"); 
                        try { 
                                Thread.sleep(50); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}
 
線程2第0次執(zhí)行! 
線程1第0次執(zhí)行! 
線程1第1次執(zhí)行! 
線程2第1次執(zhí)行! 
線程1第2次執(zhí)行! 
線程2第2次執(zhí)行! 

從上面的結(jié)果輸出可以看出,無法精準(zhǔn)保證線程執(zhí)行次序

2.4 線程交互wait,notify,notifyAll

這些操作需要配合synchronized使用

2.4.1 線程交換基礎(chǔ)

線程交互主要方法:

void notify():喚醒在此對象監(jiān)視器上等待的單個線程。
void notifyAll(): 喚醒在此對象監(jiān)視器上等待的所有線程。
void wait():導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout):導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量。
void wait(long timeout, int nanos):導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當(dāng)前線程,或者已超過某個實際時間量(timeout+nanos)

以上這些方法是幫助線程傳遞線程關(guān)心的時間狀態(tài)。

關(guān)于等待/通知,要記住的關(guān)鍵點(diǎn)是:
必須從同步環(huán)境內(nèi)調(diào)用wait()、notify()、notifyAll()方法。線程不能調(diào)用對象上等待或通知的方法,除非它擁有那個對象的鎖。
wait()、notify()、notifyAll()都是Object的實例方法。與每個對象具有鎖一樣,每個對象可以有一個線程列表,他們等待來自該信號(通知)。線程通過執(zhí)行對象上的wait()方法獲得這個等待列表。從那時候起,它不再執(zhí)行任何其他指令,直到調(diào)用對象的notify()方法為止,如果多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續(xù)執(zhí)行。如果沒有線程等待,則不采取任何特殊操作

public class ThreadA { 
    public static void main(String[] args) { 
        ThreadB b = new ThreadB(); 
        //啟動計算線程 
        b.start(); 
        //線程A擁有b對象上的鎖。線程為了調(diào)用wait()或notify()方法,該線程必須是那個對象鎖的擁有者 
        synchronized (b) { 
            try { 
                System.out.println("等待對象b完成計算。。。"); 
                //當(dāng)前線程A等待 
                b.wait(); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            System.out.println("b對象計算的總和是:" + b.total); 
        } 
    } 
} 
public class ThreadB extends Thread { 
    int total; 

    public void run() { 
        synchronized (this) { 
            for (int i = 0; i < 101; i++) { 
                total += i; 
            } 
            //(完成計算了)喚醒在此對象監(jiān)視器上等待的單個線程,在本例中線程A被喚醒 
            notify(); 
        } 
    } 
}
運(yùn)行結(jié)果:
等待對象b完成計算。。。 
b對象計算的總和是:5050 

千萬注意:
當(dāng)在對象上調(diào)用wait()方法時,執(zhí)行該代碼的線程立即放棄它在對象上的鎖。然而調(diào)用notify()時,并不意味著這時線程會放棄其鎖。如果線程仍然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,只要調(diào)用notify()并不意味著這時該鎖變得可用

2.4.2 多個線程在等待一個對象鎖時候使用notifyAll()

在多數(shù)情況下,最好通知等待某個對象的所有線程。如果這樣做,可以在對象上使用notifyAll()讓所有在此對象上等待的線程沖出等待區(qū),返回到可運(yùn)行狀態(tài)。

下面給個例子:

public class Calculator extends Thread { 
        int total; 

        public void run() { 
                synchronized (this) { 
                        for (int i = 0; i < 101; i++) { 
                                total += i; 
                        } 
                } 
                //通知所有在此對象上等待的線程 
                notifyAll(); 
        } 
}
public class ReaderResult extends Thread { 
        Calculator c; 

        public ReaderResult(Calculator c) { 
                this.c = c; 
        } 

        public void run() { 
                synchronized (c) { 
                        try { 
                                System.out.println(Thread.currentThread() + "等待計算結(jié)果。。。"); 
                                c.wait(); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                        System.out.println(Thread.currentThread() + "計算結(jié)果為:" + c.total); 
                } 
        } 

        public static void main(String[] args) { 
                Calculator calculator = new Calculator(); 

                //啟動三個線程,分別獲取計算結(jié)果 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                new ReaderResult(calculator).start(); 
                //啟動計算線程 
                calculator.start(); 
        } 
}
 
運(yùn)行結(jié)果:
Thread[Thread-1,5,main]等待計算結(jié)果。。。 
Thread[Thread-2,5,main]等待計算結(jié)果。。。 
Thread[Thread-3,5,main]等待計算結(jié)果。。。 
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner 
  at java.lang.Object.notifyAll(Native Method) 
  at threadtest.Calculator.run(Calculator.java:18) 
Thread[Thread-1,5,main]計算結(jié)果為:5050 
Thread[Thread-2,5,main]計算結(jié)果為:5050 
Thread[Thread-3,5,main]計算結(jié)果為:5050 

運(yùn)行結(jié)果表明,程序中有異常,并且多次運(yùn)行結(jié)果可能有多種輸出結(jié)果。這就是說明,這個多線程的交互程序還存在問題。究竟是出了什么問題,需要深入的分析和思考,下面將做具體分析。

實際上,上面這個代碼中,我們期望的是讀取結(jié)果的線程在計算線程調(diào)用notifyAll()之前等待即可。 但是,如果計算線程先執(zhí)行,并在讀取結(jié)果線程等待之前調(diào)用了notify()方法,那么又會發(fā)生什么呢?這種情況是可能發(fā)生的。因為無法保證線程的不同部分將按照什么順序來執(zhí)行。
當(dāng)讀取線程運(yùn)行時,它只能馬上進(jìn)入等待狀態(tài)----它沒有做任何事情來檢查等待的事件是否已經(jīng)發(fā)生。 因此,如果計算線程已經(jīng)調(diào)用了notifyAll()方法,那么它就不會再次調(diào)用notifyAll(),----并且等待的讀取線程將永遠(yuǎn)保持等待。這當(dāng)然是開發(fā)者所不愿意看到的問題。

因此針對上面的報錯需要把notifyall方法包括在synchronized方法里面,如下:

class Calculator extends Thread {
        int total;
        public void run() {
            synchronized (this) {
                for (int i = 0; i < 101; i++) {
                    total += i;
                }
                //通知所有在此對象上等待的線程
                notifyAll();
            }
        }
    }

2.5 線程鎖釋放

Java多線程運(yùn)行環(huán)境中,在哪些情況下會使對象鎖釋放?
由于等待一個鎖的線程只有在獲得這把鎖之后,才能恢復(fù)運(yùn)行,所以讓持有鎖的線程在不再需要鎖的時候及時釋放鎖是很重要的。在以下情況下,持有鎖的線程會釋放鎖:
(1)執(zhí)行完同步代碼塊,就會釋放鎖。(synchronized)
(2)在執(zhí)行同步代碼塊的過程中,遇到異常而導(dǎo)致線程終止,鎖也會被釋放。(exception)
(3)在執(zhí)行同步代碼塊的過程中,執(zhí)行了鎖所屬對象的wait()方法,這個線程會釋放鎖,進(jìn)入對象的等待池。(wait)
哪些情況不會釋放鎖?
除了以上情況外,只要持有鎖的線程還沒有執(zhí)行完同步代碼塊,就不會釋放鎖。在下面情況下,線程是不會釋放鎖的:
(1)執(zhí)行同步代碼塊的過程中,執(zhí)行了Thread.sleep()方法,當(dāng)前線程放棄CPU,開始睡眠,在睡眠中不會釋放鎖。sleep方法不考慮線程優(yōu)先級
(2)在執(zhí)行同步代碼塊的過程中,執(zhí)行了Thread.yield()方法,當(dāng)前線程放棄CPU,但不會釋放鎖。yield方法只給相同優(yōu)先級或者更高優(yōu)先級線程機(jī)會
(3)在執(zhí)行同步代碼塊的過程中,其他線程執(zhí)行了當(dāng)前線程對象的suspend()方法,當(dāng)前線程被暫停,但不會釋放鎖

2.6 sleep,yield,wait區(qū)別

sleep()是Thread類的方法,使當(dāng)前運(yùn)行中的線程睡眼一段時間,進(jìn)入不可運(yùn)行狀態(tài),這段時間的長短是由程序設(shè)定的,即使當(dāng)前線程進(jìn)入停滯狀態(tài),所以執(zhí)行sleep()的線程在指定的時間內(nèi)肯定不會被執(zhí)行,sleep方法本身會給其他線程一個運(yùn)行的機(jī)會 但是不考慮優(yōu)先級,因此會給優(yōu)先級較低的線程一個機(jī)會

yield():使當(dāng)前線程讓出CPU占有權(quán),但讓出的時間是不可設(shè)定的,只是使當(dāng)前線程重新回到可執(zhí)行狀態(tài),所以執(zhí)行yield()的線程有可能在進(jìn)入到可執(zhí)行狀態(tài)后馬上又被執(zhí)行。實際上,yield()方法對應(yīng)了如下操作:先檢測當(dāng)前是否有相同優(yōu)先級的線程處于同可運(yùn)行狀態(tài),如有,則把CPU的占有權(quán)交給此線程,否則,繼續(xù)運(yùn)行原來的線程。所以yield()方法稱為退讓,它把運(yùn)行機(jī)會讓給了同等優(yōu)先級的其他線程

wait是Object類的方法,用來線程間的通信,對此對象調(diào)用wait方法導(dǎo)致本線程放棄對象鎖,進(jìn)入等待此對象的等待鎖定池,使線程所在對象中的其它synchronized數(shù)據(jù)可被別的線程使用,只有針對此對象發(fā)出notify方法(或notifyAll)后本線程才進(jìn)入對象鎖定池準(zhǔn)備獲得對象鎖進(jìn)入運(yùn)行狀態(tài)
wait()和notify()因為會對對象的鎖標(biāo)志進(jìn)行操作,所以它們必須在synchronized函數(shù)或synchronized block中進(jìn)行調(diào)用。如果在non-synchronized函數(shù)或non-synchronized block中進(jìn)行調(diào)用,雖然能編譯通過,但在運(yùn)行時會發(fā)生 IllegalMonitorStateException的異常。

3 其他線程方面

3.1 守護(hù)線程

3.1.1 守護(hù)線程定義

在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護(hù)線程)
用個比較通俗的假設(shè),任何一個守護(hù)線程都是整個JVM中所有非守護(hù)線程的保姆。只要當(dāng)前JVM實例中尚存在任何一個非守護(hù)線程沒有結(jié)束,守護(hù)線程就全部工作;只有當(dāng)最后一個非守護(hù)線程結(jié)束時,守護(hù)線程隨著JVM一同結(jié)束工作。Daemon的作用是為其他線程的運(yùn)行提供便利服務(wù),守護(hù)線程最典型的應(yīng)用就是 GC (垃圾回收器),它就是一個很稱職的守護(hù)者

守護(hù)線程與普通線程寫法上基本么啥區(qū)別,調(diào)用線程對象的方法setDaemon(true),則可以將其設(shè)置為守護(hù)線程,但是必須在thread.start()之前設(shè)置

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

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)前線程無法修改該線程。

3.1.3 實際操作

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(); 
                        } 
                } 
        } 
}
 
后臺線程第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í)行完畢就退出了。
實際上:JRE判斷程序是否執(zhí)行結(jié)束的標(biāo)準(zhǔn)是所有前臺執(zhí)線程行完畢,而不管后臺線程的狀態(tài),因此,在使用后臺線程時候一定要注意這個問題。

3.2 線程優(yōu)先級

與線程休眠類似,線程的優(yōu)先級仍然無法保障線程的執(zhí)行次序。只不過,優(yōu)先級高的線程獲取CPU資源的概率較大,優(yōu)先級低的并非沒機(jī)會執(zhí)行。

線程的優(yōu)先級用1-10之間的整數(shù)表示,數(shù)值越大優(yōu)先級越高,默認(rèn)的優(yōu)先級為5

在一個線程中開啟另外一個新線程,則新開線程稱為該線程的子線程,子線程初始優(yōu)先級與父線程相同。

public class Test { 
        public static void main(String[] args) { 
                Thread t1 = new MyThread1(); 
                Thread t2 = new Thread(new MyRunnable()); 
                t1.setPriority(10); 
                t2.setPriority(1); 

                t2.start(); 
                t1.start(); 
        } 
} 
class MyThread1 extends Thread { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("線程1第" + i + "次執(zhí)行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
} 
class MyRunnable implements Runnable { 
        public void run() { 
                for (int i = 0; i < 10; i++) { 
                        System.out.println("線程2第" + i + "次執(zhí)行!"); 
                        try { 
                                Thread.sleep(100); 
                        } catch (InterruptedException e) { 
                                e.printStackTrace(); 
                        } 
                } 
        } 
}
 
線程1第0次執(zhí)行! 
線程2第0次執(zhí)行! 
線程2第1次執(zhí)行! 
線程1第1次執(zhí)行! 
線程2第2次執(zhí)行! 
線程1第2次執(zhí)行! 
線程1第3次執(zhí)行! 
線程2第3次執(zhí)行! 
線程2第4次執(zhí)行! 
線程1第4次執(zhí)行! 
線程1第5次執(zhí)行! 
線程2第5次執(zhí)行! 
線程1第6次執(zhí)行! 
線程2第6次執(zhí)行! 
線程1第7次執(zhí)行! 
線程2第7次執(zhí)行! 
線程1第8次執(zhí)行! 
線程2第8次執(zhí)行! 
線程1第9次執(zhí)行! 
線程2第9次執(zhí)行! 

3.3 線程棧模型

要理解線程調(diào)度的原理,以及線程執(zhí)行過程,必須理解線程棧模型。
線程棧 是指某時刻 時 內(nèi)存中線程調(diào)度的棧信息,當(dāng)前調(diào)用的方法總是位于 棧頂。線程棧的內(nèi)容是隨著程序的運(yùn)行動態(tài)變化的,因此研究線程棧必須選擇一個運(yùn)行的時刻(實際上指代碼運(yùn)行到什么地方)。

下面通過一個示例性的代碼說明線程(調(diào)用)棧的變化過程

在這里插入圖片描述
這幅圖描述在代碼執(zhí)行到兩個不同時刻1、2時候,虛擬機(jī)線程調(diào)用棧示意圖。

當(dāng)程序執(zhí)行到t.start();時候,程序多出一個分支(增加了一個調(diào)用棧B),這樣,棧A、棧B并行執(zhí)行。
從這里就可以看出方法調(diào)用和線程啟動的區(qū)別了
3.4 線程結(jié)束
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運(yùn)行的方法已經(jīng)被廢棄了,使用它們是極度不安全的!想要安全有效的結(jié)束一個線程,可以使用下面的方法。

正常執(zhí)行完run方法后結(jié)束掉
控制循環(huán)條件和判斷條件的標(biāo)識符來結(jié)束掉線程
使用interrupt結(jié)束一個線程

具體操作如下

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);  
        }  
    }  
}  

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

使用第2中方法的標(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了  

可以看到,首先執(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)容

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