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)圖示:
線程生命狀態(tài)
新建狀態(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異常。就緒狀態(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í)行子線程。運(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)該盡量避免使用該方法。阻塞狀態(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)死亡狀態(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異常