問題
在《如何創建多線程,如何理解線程之間的優先級》一文中,已經提到如果不對程序中的兩個線程進行合理地調度,則會輸出不可預測的結果,因此就要求必須采用一些特殊的方法來有效地控制線程。但是,利用線程優先級顯然是一種不可靠的方案,那麼如何既保證資源的有效利用,又能保證程序的結果能夠按照預期的那樣被正確地輸出呢?
解決思路
多線程的引入往往使程序顯示出無法預料的結果,這是因為線程和線程之間在獲得CPU時間片的機會上沒有一定的規律。通常情況下當多線程創建以後,都不會任其執行,而是為達到特定目的有效地調度這些線程。Java提供了一系列控制線程的方法和手段,使之能在充分利用系統資源的前提下,還能夠保證程序結果按照要求被正確地輸出和顯示。這些控制的手段有:等待一個線程結束、合並線程、設置守護線程以及中斷一個線程等。下面就一一介紹這些控制線程的手段。
為了能夠更好地控制線程,實現線程的合理調度,首先需要了解一下線程的生命周期。一個線程從創建到消亡存在四種狀態,分別介紹如下。
1 / 創建狀態(new Thread)
當一個線程被實例化後,它就處於創建狀態,直到調用 start()方法。當一個線程處於創建狀態時,它僅僅是一個空的線程對象,系統不為它分配資源。在這個狀態下,線程還不是活(alive)的。
要想使線程處於創建狀態,可以通過執行下面的語句來實現:
Thread myThread = new MyThreadClass();
2 / 可運行狀態(Runnable)
對於處於創建狀態的線程來說,只要對其調用了start()方法,就可以使該線程進入可運行狀態。當一個線程處於可運行狀態時,系統為這個線程分配它所需要的系統資源,然後安排其運行並調用線程的run()方法。此時的線程具備了所有能夠運行的條件,但是,此狀態的線程並不一定馬上就被執行。這是因為目前所使用的計算機大多都是單處理器的,因此,要想在同一時刻運行所有的處於可運行狀態的線程是不可能的。這時還需要Java的運行系統(JRE)通過合理的調度策略才能使此狀態下的線程真正得到CPU。不過線程在這種狀態下已經被視為是活(alive)的了。
要想使線程處於可運行狀態,可以通過執行下面的語句來實現:
myThread.start();
3 / 不可運行狀態(Not Runnable)
可以認為,在下面列出的條件之一發生時,線程進入不可運行狀態。
◆調用了sleep()方法;
◆為等候一個條件變量,線程調用了wait()方法;
◆輸入輸出流中線程被阻塞。
不可運行狀態也可以稱為阻塞狀態(Blocked)。導致線程阻塞的原因有很多,比如等待消息,輸入輸出不合理等。此時,即使處理器空閒,系統也無法執行這些處於阻塞狀態的線程,除非阻塞條件被破壞,如:sleep的時間到達、得到條件變量之後使用了notify()方法或者 notifyAll()方法喚醒了waiting中的一個或所有線程、獲得了需要的I/O資源等。這些方法會在後面進行介紹。在這種狀態下,線程也被視為是alive的。
4 / 消亡狀態(Dead)
一般來講,線程的run()方法執行完畢時,該線程就被視為進入了消亡狀態。一個處於消亡狀態的線程不能再進入其他狀態,即使對它調用 start()方法也無濟於事。可以通過兩種方法使線程進入消亡狀態:自然撤消(線程執行完)或是強行中止(比如,調用了Thread類的stop()方法)。需要說明的是,stop()方法屬於Java的早期版本,現在已作為保留的方法不再使用,因此要終止一個線程,需要通過其他方法來實現,如interrupt()。這一方法將在第4.4節中介紹。
下面的圖4.3.1描繪出了線程從創建到消亡的整個過程,讀者可以結合前面分析的每個階段來把握線程的生命周期。
了解線程的生命周期對更好地控制線程是很有幫助的,因為可以控制每一個線程使其處於指定的狀態,從而完成特定的任務。但是也會因為方法使用的不當而使線程表現出更加難以預料的結果,比如本想使一個線程休眠等待,卻將其結束等。這就要求不僅要對線程的生命周期有很好的理解,而且還能夠調用正確的方法來控制線程。這一節就介紹一種控制線程的方法:如何等待一個線程結束。
圖4.3.1 線程的生命周期具體步驟
在現實問題域中,有可能需要等待一個線程執行結束後再運行另一個線程,這時可以利用Java提供的兩種方法來實現這個功能。
(1)調用線程類的isAlive()方法和sleep()方法來等待某個或某些線程結束
下面這個例子將創建兩個線程,並分別依次輸出兩個直角三角形。
// 例4.3.1 ThreadTestDemo.Java
class NewThread implements Runnable
{
public void run() // 重載run方法
{
System.out.println(Thread.currentThread().getName());
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print("*");
}
System.out.println(); // 顯示完一行以後換行輸出
}
}
}
class ThreadTestDemo
{
public static void main(String[] args) // 創建兩個線程
{
NewThread n = new NewThread();
Thread t1 = new Thread(n,"thread1");
Thread t2 = new Thread(n,"thread2");
t1.start();
t2.start();
}
}
題目要求先運行第一個線程t1,輸出一個直角三角形,然後等待第一個線程終止後再運行第二個線程t2,再輸出一個直角三角形。而實際運行的結果如何呢?編譯並運行這個程序,就會發現,實際運行的結果並不是兩個直角三角形,而是一些亂七八糟的“*”號行,有的長,有的短。為什麼會這樣呢?很顯然,因為線程並沒有按照預期的調用順序來執行,而是產生了線程賽跑的現象。實際上前面已經多次提到,Java在不進行任何控制的情況下不可能按照調用順序來執行線程,而是交替執行。如果要想得到預期的結果,就需要對這兩個線程加以適當的控制,讓第一個線程先執行,並判斷第一個線程是否已經終止,如果已經終止,再調用第二個線程來執行。
解決這個問題的方案是通過調用Thread類的isAlive()方法來實現題目要求。它的語法格式如下所示:
public final Boolean isAlive()
該方法可以用來測試當前線程是否仍處於活動狀態,如果處於活動狀態,則返回true,保持該線程繼續運行,否則返回false,結束該線程並做其他處理。
對例4.3.1的代碼做一些改動,在主類中引入isAlive()方法,改動後的代碼如下所示:
// 例4.3.2 ThreadTestDemo2.Java
class NewThread implements Runnable
{
… //省略
}
class ThreadTestDemo2
{
public static void main(String[] args) //創建兩個線程
{
NewThread n = new NewThread();
Thread t1 = new Thread(n,"thread1");
Thread t2 = new Thread(n,"thread2");
t1.start(); // 執行第一個線程
while(t1.isAlive()) // 不斷查詢第一個線程的狀態
{
try
{
Thread.sleep(100); // 讓主線程休眠100毫秒,程序不往下執行
}catch(InterruptedException e){ }
}
t2.start(); //第一個線程終止,運行第二個線程
}
}
本程序中,先創建了兩個線程t1和t2,使其進入創建狀態,然後啟動線程t1,使其進入可運行狀態,通過調度使其得到CPU從而先被執行。如果線程t1未執行結束,則使用sleep()方法讓主線程休眠,使其進入不可運行狀態,一直等待線程t1執行結束。一旦t1執行結束,該線程就進入消亡態,此時線程t1的狀態從alive變成dead。循環測試結束,主線程從不可運行態回到可運行態,從而得到CPU的控制權,執行t2.start()方法,啟動t2線程。接著主線程執行完畢,進入消亡態,而線程t2響應start()方法進入可運行態,根據調度策略獲得CPU控制權,輸出第二個直角三角形。最後線程t2自然結束,進入消亡態,整個程序結束。
執行這個程序,結果依次輸出了兩個直角三角形,這說明isAlive()方法和循環語句配合使用可以通過判斷一個線程是否結束,實現使其他線程等待的目的。
(2)調用線程類的join()方法來等待某個或某些線程結束
Thread類中的join()方法也可以用來等待一個線程的結束,而且這個方法更為常用,它的語法格式如下所示:
public final void join() throws InterruptedException
該方法將使得當前線程等待調用該方法的線程結束後, 再恢復執行。由於該方法被調用時可能拋出一個InterruptedException異常,因此在調用它的時候需要將它放在try…catch語句中。對前面的程序做一些改動,引入join()方法,並觀察程序運行的結果是否有所變化。改動後的代碼如下所示:
// 例4.3.3 ThreadTestDemo3.Java
class NewThread implements Runnable
{
… //省略
}
class ThreadTestDemo3
{
public static void main(String[] args) //創建兩個線程
{
NewThread n = new NewThread();
Thread t1 = new Thread(n,"thread1");
Thread t2 = new Thread(n,"thread2");
t1.start(); // 執行第一個線程
try
{
t1.join(); //當前線程等待線程t1 執行完後再繼續往下執行
}catch(InterruptedException e){
e.printStackTrace();
}
t2.start(); //第一個線程結束,運行第二個線程
}
}
編譯並運行程序,可以看到,輸出的結果和例4.3.2完全相同,也是依次輸出了兩個直角三角形。這是怎麼實現的呢?在try…catch語句中,程序調用了t1.join()方法,該方法將使t1這個線程合並到調用t1.join();語句的線程中,在該程序中這個線程就是主線程。一旦t1合並到主線程以後,程序就一直執行線程t1,而主線程進入不可運行態,並一直這樣等待,直到線程t1執行結束為止,主線程恢復,進入可運行態,程序才執行t2.start(),啟動線程t2。此外也可以看出,使用join()方法來等待一個線程比使用isAlive()方法更加簡潔。
通過查看JDK幫助文檔,還可以看到,在Thread類中,除了一個無參的join()方法之外,還有兩個帶參的join()方法,它們分別是join(long millis)和join(long millis,int nanos)。使用它們不僅可以合並線程,而且還指定了合並的時間,前者精確到毫秒,後者精確到納秒。當合並時間到達時,兩個合並的線程會恢復到合並前的狀態。當然,使用這兩個方法的時候仍需要將它們放在try…catch語句中以處理可能發生的異常。
這三個join()方法其實很像是將while循環、isAlive()方法以及sleep()方法在功能上進行的組合,只不過join()方法將它們給封裝了起來。因此,使用該方法等待一個線程結束將使程序更加簡練,值得提倡。
專家說明
通過前面的學習,掌握了如何實現等待一個線程結束的方法。其中,一種方法是通過while循環使用isAlive()方法和sleep()方法配合來實現,一種方法是通過在程序中調用join()方法來實現。這兩種方法表現的結果是相同的,讀者可以根據實際情況任選其一。但是也應該看到,join()方法在解決這類問題時更有優勢。
專家指點
在本節中,通過對線程生命周期的了解將對線程的執行過程中所處的每一個狀態有深刻的認識,這就保證了編寫多線程的Java程序時,能夠更好地了解程序中每個線程在運行當中可能處於的狀態,從而很好地控制線程。其實,Java的Thread類中提供了很多能夠控制線程的方法。這些方法會在本章後面的小節中以問題的形式一一介紹。讀者一定要善於利用這些方法,使程序不僅能夠被執行,而且能夠被正確實現。
此外,通過本節中所給出的例子還發現,程序中即使主線程結束,如果一般線程還 未結束,程序仍然不會結束,這顯然是不安全的。那麼怎麼樣才能保證當主線程結束時,所有其他線程無論執行到什麼狀態都必須隨之一起結束呢?在相關問題中將回答這一問題。
相關問題
在Java程序當中,可以把線程分為兩類:用戶線程和守護線程(又稱為後台線程)。用戶線程是那些完成有用工作的線程,也就是前面所說的一般線程。守護線程是那些僅提供輔助功能的線程。這類線程可以監視其他線程的運行情況,也可以處理一些相對不太緊急的任務。在一些特定的場合,經常會通過設置守護線程的方式來配合其他線程一起完成特定的功能。
Thread 類提供了setDaemon()方法用來打開或者關閉一個線程的守護狀態(Daemon)。通過Thread類提供的isDaemon()方法,還可查看一個線程是不是一個處於守護狀態的線程。如果是一個Daemon線程,那麼它創建的任何線程也會自動具備Daemon屬性。
下面先來看一個簡單的程序演示Daemon線程的用法:
// 例 4.3.4 DaemoDemo.Java
import Java.io.*;
class MyThread extends Thread
{
public void run()
{
for(int i=0;i<100;i++)
{
System.out.println("NO. "+i+" Daemon is "+isDaemon());
}
}
}
class DaemonDemo
{
public static void main(String[] args) throws IOException
{
System.out.println("Thread's daemon status,yes(Y) or no(N): ");
BufferedReader stdin = new BufferedReader(new
InputStreamReader(System.in)); //建立緩沖字符流
String str;
str = stdin.readLine(); // 從鍵盤讀取一個字符串
if(str.equals("yes") || str.equals("Y"))
{
MyThread t = new MyThread();
t.setDaemon(true); // 設置該線程為守護線程
t.start();
}
else
new MyThread().start(); // 該線程為用戶線程
}
}
運行程序,從鍵盤輸入一個字符串yes或者Y的時候,程序將創建一個守護線程。緊接著主線程執行結束,守護線程也隨之消亡,此時在線程的run()方法中循環語句剛開始執行就結束了,這就說明守護線程隨用戶線程結束而一起結束,無論其執行是否完畢。如果從鍵盤輸入一個字符串no或者N的時候,程序將創建一個用戶線程。這樣,不管主線程是否結束,該用戶線程都要執行循環100次,而且通過輸出的線程狀態是:Daemon is false,也說明了該線程不是守護線程,可在主線程結束之後繼續運行直到run()方法執行結束為止。
該程序中無論創建用戶線程還是創建守護線程,都只有兩個線程在運行,那麼如果有更多的線程在執行呢?下面再舉一個例子來演示Daemon線程的用法:該例子源於《Java編程思想》一書,中間有一些代碼做了改動,並添加了注釋,以供讀者參考。
import Java.io.*;
class Daemon extends Thread
{
private static final int LENGTH = 10; //定義一個常量LENGTH
private Thread[] thr = new Thread[LENGTH]; //定義一個線程數組
public Daemon() //構造方法
{
setDaemon(true); // 設置當前線程為守護線程
start(); //啟動該守護線程
}
public void run() // 線程體
{
for(int i = 0; i < LENGTH; i++) //創建LENGTH個Daemons類的線程對象
thr[i] = new Daemons(i);
for(int i = 0; i < LENGTH; i++) // 輸出每一個線程狀態是否為Daemon
System.out.println( "thr[" + i + "].isDaemon() = "
+ thr[i].isDaemon());
}
}
class Daemons extends Thread
{
private int i;
public Daemons(int i) //構造方法
{
this.i = i;
start();
}
public void run()
{
System.out.println("Daemons " + i + " started");
}
}
public class DaemonDemo // 後台線程主類
{
public static void main(String[] args)
{
Daemon t = new Daemon(); // 創建一個線程
System.out.println("t.isDaemon() = " + t.isDaemon());
// 建立標准緩沖字符流
BufferedReader stdin =
new BufferedReader(new InputStreamReader(System.in));
System.out.println("Waiting for CR");
try
{
stdin.readLine(); // 從鍵盤獲取一行字符串
// Thread.sleep(10);
} catch(IOException e) {
e.printStackTrace();
}
}
}
程序中,Daemon線程先將自己的Daemon標記設置成“真”,然後產生一系列線程,因而這些線程也具有Daemon屬性。一旦main()方法完成自己的工作,便沒有什麼能阻止程序中斷運行,因為這裡運行的只有Daemon線程。所以能看到啟動所有Daemon線程後顯示出來的結果,System.in也進行了相應的設置,使程序中斷前能等待一個回車。如果不進行這樣的設置,就只能看到創建Daemon線程的一部分結果。
將readLine()代碼換成不同長度的sleep()調用,catch子句的參數聲明為Interrupted- Exception異常,看看會有什麼表現。結果顯示守護線程還沒有創建完成,整個程序就結束了,從這一點也說明了守護線程不是程序的基本部分,當非Daemon線程全部結束以後,程序就會中止運行,而不管是否還有Daemon線程的存在。
“Daemon”線程的作用就是在程序的運行期間於後台提供一種“常規”服務,但它並不屬於程序的一個基本部分。在 Java 虛擬機 (JVM) 中,即使在 main 線程結束以後,如果另一個用戶線程仍在運行,則程序仍然可以繼續運行。Java 程序將運行到所有用戶線程終止,然後它將破壞所有的守護線程。因此,一旦所有非Daemon線程完成,程序也會中止運行。