| 導購 | 订阅 | 在线投稿
分享
 
 
 

用Java線程獲取優異性能(I)??介紹線程、線程類及Runnable

來源:互聯網網民  2008-05-31 12:10:20  評論

用Java線程獲取優異性能(I)

摘要

用戶期望程序能展現優異的性能。爲了滿足這個期望,你的程序經常使用到線程。在這篇文章中我們開始練習使用線程。你將學習到線程、線程類及Runnable。

用戶不喜歡反應遲鈍的軟件。當用戶單擊一個鼠標時,他們希望程序立即回應他們的請求,即使程序正處于費時的運行之中,比如爲一篇很長的文檔重編頁碼或等待一個網絡操作的完成。對用戶響應很慢的程序其性能拙劣。爲提高程序性能,開發者一般使用線程。

這篇文章是探索線程的第一部份。雖然你可能認爲線程是一種難于把握的事物,但我打算向你顯示線程是易于理解的。在這篇文章中,我將向你介紹線程和線程類,以及討論Runnable。此外,在後面的文章中,我將探索同步(通過鎖),同步的問題(比如死鎖),等待/通知機制,時序安排(有優先權和沒有優先權),線程中斷,計時器,揮發性,線程組和線程本地變量。

閱讀關于線程設計的整個系列:

?第1部份:介紹線程和線程類,以及Runnable

?第2部份:使用同步使線程串行化訪問要害代碼部份

注重

這篇文章及其應用程序的三個相關線程練習與applets不同。然而,我在應用程序中介紹的多數應用到applets。主要不同的是:爲了安全的原因,不是所有的線程操作都可以放到一個applet中(我將在以後的文章中討論applets)。

什麽是線程?

線程的概念並不難于把握:它是程序代碼的一個獨立的執行通道。當多個線程執行時,經由相同代碼的一個線程的通道通常與其它的不同。例如,假設一個線程執行一段相當于一個if-else語句的if部分的字節代碼時,而另一個線程正執行相當于else部分的字節代碼。JVM怎樣保持對于每一個線程執行的跟蹤呢?JVM給每一個線程它自己的方法調用堆棧。另外跟蹤當前指令字節代碼,方法堆棧跟蹤本地變量,JVM傳遞給一個方法的參數,以及方法的返回值。

當多個線程在同一個程序中執行字節代碼序列時,這種行爲叫作多線程。多線程在多方面有利于程序:

?當執行其它任務時多線程GUI(圖形用戶界面)程序仍能保持對用戶的響應,比如重編頁碼或打印一個文檔。

?帶線程的程序一般比它們沒有帶線程的副本程序完成得快。這尤其表現在線程運行在一個多處理器機器上,在這裏每一個線程都有它自己的處理器。

Java通過java.lang.Thread類完成多線程。每一個線程對象描述一個單獨的執行線程。那些運行發生在線程的run()方法中。因爲缺省的run()方法什麽都不做,你必須創建Thread子類並重載run()以完成有用的工作。練習列表1中領略一個在Thread中的線程及多線程:

列表1. ThreadDemo.java

// ThreadDemo.java

class ThreadDemo

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

for (int i = 0; i

System.out.println ("i = " + i + ", i * i = " + i * i);

}

}

class MyThread extends Thread

{

public void run ()

{

for (int count = 1, row = 1; row

{

for (int i = 0; i

System.out.print ('*');

System.out.print ('

');

}

}

}

列表1顯示了一個由類ThreadDemo和MyThread組成的應用程序的源代碼。類ThreadDemo通過創建一個MyThread對象驅動應用程序,開始一個與其對象相關的線程並執行一段打印一個正方形表的代碼。相反, MyThread重載Thread的run()方法打印(通過標准輸入流)一個由星形符號組成的直角三角形。

當你鍵入java ThreadDemo運行應用程序時, JVM創建一個運行main()方法的開始線程。通過執行mt.start (),開始線程告訴JVM創建一個執行包含MyThread對象的run()方法的字節代碼指令的第二個線程。當start()方法返回時,開始線程循環執行打印一個正方形表,此時另一個新線程執行run()方法打印直角三角形。

輸出會象什麽樣呢?運行ThreadDemo就可以看到。你將注重到每一個線程的輸出與其它線程的輸出相互交替。這樣的結果是因爲兩個線程將它們的輸出都發送到了同樣的標准輸出流。

注重

多數(不是所有)JVM設備使用下層平台的線程性能。因爲那些性能是平台特有的,你的多線程程序的輸出順序可能與一些人的其他輸出的順序不一樣。這種不同是由于時序的安排,我將在這一系列的稍後探討這一話題。

線程類

要精通寫多線程代碼,你必須首先理解創建Thread類的多種方法。這部份將探討這些方法。明確地說,你將學到開始線程的方法,命名線程,使線程休眠,決定一個線程是否激活,將一個線程與另一個線程相聯,和在當前線程的線程組及子組中列舉所有激活的線程。我也會討論線程調試輔助程序及用戶線程與監督線程的對比。

我將在以後的文章中介紹線程方法的余下部份,Sun不贊成的方法除外。

警告

Sun有一些不贊成的線程方法種類,比如suspend()和resume(),因爲它們能鎖住你的程序或破壞對象。所以,你不必在你的代碼中調用它們。考慮到針對這些方法工作區的SDK文件,在這篇文章中我沒有包含這些方法。

構造線程

Thread有八個構造器。最簡單的是:

?Thread(),用缺省名稱創建一個Thread對象

?Thread(String name),用指定的name參數的名稱創建一個Thread對象

下一個最簡單的構造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable參數之外,這些構造器與前述的構造器一樣。不同的是:Runnable參數識別提供run()方法的線程之外的對象。(你將在這篇文章稍後學到Runnable。)最後幾個構造器是Thread(String name),Thread(Runnable target),和Thread(Runnable target, String name)。然而,最後的構造器包含了一個爲了組織意圖的ThreadGroup參數。

最後四個構造器之一,Thread(ThreadGroup group, Runnable target, String name, long stackSize),令人感愛好的是它能夠讓你指定想要的線程方法調用堆棧的大小。能夠指定大小將證實在使用遞歸方法(一種爲何一個方法不斷重複調用自身的技術)美麗地解決一些問題的程序中是十分有幫助的。通過明確地設置堆棧大小,你有時能夠預防StackOverflowErrors。然而,太大將導致OutOfMemoryErrors。同樣,Sun將方法調用堆棧的大小看作平台依靠。依靠平台,方法調用堆棧的大小可能改變。因此,在寫調用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代碼前仔細考慮你的程序分枝。

開始你的運載工具

線程類似于運載工具:它們將程序從開始移動到結束。Thread 和Thread子類對象不是線程。它們描述一個線程的屬性,比如名稱和包含線程執行的代碼(經由一個run()方法)。當一個新線程執行run()時,另一個線程正調用Thread或其子類對象的start()方法。例如,要開始第二個線程,應用程序的開始線程?它執行main()?調用start()。作爲響應,JVM和平台一起工作的線程操作代碼確保線程正確地初始化並調用Thread或其子類對象的run()方法。

一旦start()完成,多重線程便運行。因爲我們趨向于在一種線性的方式中思維,我們常發現當兩個或更多線程正運行時理解並發(同時)行爲是困難的。因此,你應該看看顯示與時間對比一個線程正在哪裏執行(它的位置)的圖表。下圖就是這樣一個圖表。

與時間對比一個開始線程和一個新建線程執行位置的行爲

圖表顯示了幾個重要的時間段:

?開始線程的初始化

?線程開始執行main()瞬間

?線程開始執行start()的瞬間

?start()創建一個新線程並返回main()的瞬間

?新線程的初始化

?新線程開始執行run()的瞬間

?每個線程結束的不同瞬間

注重新線程的初始化,它對run()的執行,和它的結束都與開始線程的執行同時發生。

警告

一個線程調用start()後,在run()方法退出前並發調用那方法將導致start()擲出一個java.lang.IllegalThreadStateException對象。

怎樣使用名稱

在一個調試會話期間,使用用戶友好方式從另一個線程區別其中一個線程證實是有幫助的。要區分其中一個線程,Java給一個線程取一個名稱。Thread缺省的名稱是一個短線連字符和一個零開始的數字符號。你可以接受Java的缺省線程名稱或選擇使用你自己的。爲了能夠自定義名稱,Thread提供帶有name參數和一個setName(String name)方法的構造器。Thread也提供一個getName()方法返回當前名稱。表2顯示了怎樣通過Thread(String name)創建一個自定義名稱和通過在run()方法中調用getName()檢索當前名稱:

表2.NameThatThread.java

// NameThatThread.java

class NameThatThread

{

public static void main (String [] args)

{

MyThread mt;

if (args.length == 0)

mt = new MyThread ();

else

mt = new MyThread (args [0]);

mt.start ();

}

}

class MyThread extends Thread

{

MyThread ()

{

//編譯器創建等價于super()的字節代碼

}

MyThread (String name)

{

super (name); //將名稱傳遞給Thread超類

}

public void run ()

{

System.out.println ("My name is: " + getName ());

}

}

你能夠在命令行向MyThread傳遞一個可選的name參數。例如,java NameThatThread X 建立X作爲線程的名稱。假如你指定一個名稱失敗,你將看到下面的輸出:

My name is: Thread-1

假如你喜歡,你能夠在MyThread(String name)構造器中將super(name)調用改變成setName(String name)調用??作爲setName(name)後一種方法調用達到同樣建立線程名稱的目的??作爲super(name)我作爲練習保留給你們。

注重

Java主要將名稱指派給運行main() 方法的線程,開始線程。你非凡要看看當開始線程擲出一個例外對象時在線程「main」的例外顯示的JVM的缺省例外處理打印消息。

休眠或停止休眠

在這一欄後面,我將向你介紹動畫??在一個表面上重複畫圖形,這稍微不同于完成一個運動畫面。要完成動畫,一個線程必須在它顯示兩個連續畫面時中止。調用Thread的靜態sleep(long millis)方法強迫一個線程中止millis毫秒。另一個線程可能中斷正在休眠的線程。假如這種事發生,正在休眠的線程將醒來並從sleep(long millis)方法擲出一個InterruptedException對象。結果,調用sleep(long millis)的代碼必須在一個try代碼塊中出現??或代碼方法必須在自己的throws子句中包括InterruptedException。

爲了示範sleep(long millis),我寫了一個CalcPI1應用程序。這個應用程序開始了一個新線程便于用一個數學運算法則計算數學常量pi的值。當新線程計算時,開始線程通過調用sleep(long millis)中止10毫秒。在開始線程醒後,它將打印pi的值,其中新線程存貯在變量pi中。表3給出了CalcPI1的源代碼:

表3. CalcPI1.java

// CalcPI1.java

class CalcPI1

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

try

{

Thread.sleep (10); //休眠10毫秒

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化爲0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

假如你運行這個程序,你將看到輸出如下(但也可能不一樣):

pi = -0.2146197014017295

完成計算PI

爲什麽輸出不正確呢?究竟,pi的值應近似等于3.14159。回答是:開始線程醒得太快了。在新線程剛開始計算pi時,開始線程就醒過來讀取pi的當前值並打印其值。我們可以通過將10毫秒延遲增加爲更長的值來進行補償。這一更長的值(不幸的是它是依靠于平台的)將給新線程一個機會在開始線程醒過來之前完成計算。(後面,你將學到一種不依靠平台的技術,它將防止開始線程醒來直到新線程完成。)

注重

線程同時提供一個sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因爲多數基于JVM的平台都不支持納秒級的分解度,JVM 線程處理代碼將納秒數字四舍五入成毫秒數字的近似值。假如一個平台不支持毫秒級的分解度,JVM 線程處理代碼將毫秒數字四舍五入成平台支持的最小級分解度的近似倍數。

它是死的還是活的?

當一個程序調用Thread的start()方法時,在一個新線程調用run()之前有一個時間段(爲了初始化)。run()返回後,在JVM清除線程之前有一段時間通過。JVM認爲線程立即激活優先于線程調用run(),在線程執行run()期間和run()返回後。在這時間間隔期間,Thread的isAlive()方法返回一個布爾真值。否則,方法返回一個假值。

isAlive()在一個線程需要在第一個線程能夠檢查其它線程的結果之前等待另一個線程完成其run()方法的情形下證實是有幫助的。實質上,那些需要等待的線程輸入一個while循環。當isAlive()爲其它線程返回真值時,等待線程調用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費更多的CPU循環)。一旦isAlive()返回假值,等待線程便檢查其它線程的結果。

你將在哪裏使用這樣的技術呢?對于起動器,一個CalcPI1的修改版本怎麽樣,在打印pi的值前開始線程在哪裏等待新線程的完成?表4的CalcPI2源代碼示範了這一技術:

表4. CalcPI2.java

// CalcPI2.java

class CalcPI2

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

while (mt.isAlive ())

try

{

Thread.sleep (10); //休眠10毫秒

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化成0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

CalcPI2的開始線程在10毫秒時間間隔休眠,直到mt.isAlive ()返回假值。當那些發生時,開始線程從它的while循環中退出並打印pi的內容。假如你運行這個程序,你將看到如下的輸出(但不一定一樣):

完成計算PI

pi = 3.1415726535897894

這不,現在看上去更精確了?

注重

一個線程可能對它自己調用isAlive() 方法。然而,這毫無意義,因爲isAlive()將一直返回真值。

合力

因爲while循環/isAlive()方法/sleep()方法技術證實是有用的,Sun將其打包進三個方法組成的一個組合裏:join(),join(long millis)和join(long millis, int nanos)。當當前線程想等待其它線程結束時,經由另一個線程的線程對象引用調用join()。相反,當它想其中任意線程等待其它線程結束或等待直到millis毫秒和nanos納秒組合通過時,當前線程調用join(long millis)或join(long millis, int nanos)。(作爲sleep()方法,JVM 線程處理代碼將對join(long millis)和join(long millis,int nanos)方法的參數值四舍五入。)表5的CalcPI3源代碼示範了一個對join()的調用:

表5. CalcPI3.java

// CalcPI3.java

class CalcPI3

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

try

{

mt.join ();

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化成0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

CalcPI3的開始線程等待與MyThread對象有關被mt引用的線程結束。接著開始線程打印pi的值,其值與CalcPI2的輸出一樣。

警告

不要試圖將當前線程與其自身連接,因爲這樣當前線程將要永遠等待。

怎樣使用名稱

在一個調試會話期間,使用用戶友好方式從另一個線程區別其中一個線程證實是有幫助的。要區分其中一個線程,Java給一個線程取一個名稱。Thread缺省的名稱是一個短線連字符和一個零開始的數字符號。你可以接受Java的缺省線程名稱或選擇使用你自己的。爲了能夠自定義名稱,Thread提供帶有name參數和一個setName(String name)方法的構造器。Thread也提供一個getName()方法返回當前名稱。表2顯示了怎樣通過Thread(String name)創建一個自定義名稱和通過在run()方法中調用getName()檢索當前名稱:

表2.NameThatThread.java

// NameThatThread.java

class NameThatThread

{

public static void main (String [] args)

{

MyThread mt;

if (args.length == 0)

mt = new MyThread ();

else

mt = new MyThread (args [0]);

mt.start ();

}

}

class MyThread extends Thread

{

MyThread ()

{

//編譯器創建等價于super()的字節代碼

}

MyThread (String name)

{

super (name); //將名稱傳遞給Thread超類

}

public void run ()

{

System.out.println ("My name is: " + getName ());

}

}

你能夠在命令行向MyThread傳遞一個可選的name參數。例如,java NameThatThread X 建立X作爲線程的名稱。假如你指定一個名稱失敗,你將看到下面的輸出:

My name is: Thread-1

假如你喜歡,你能夠在MyThread(String name)構造器中將super(name)調用改變成setName(String name)調用??作爲setName(name)後一種方法調用達到同樣建立線程名稱的目的??作爲super(name)我作爲練習保留給你們。

注重

Java主要將名稱指派給運行main() 方法的線程,開始線程。你非凡要看看當開始線程擲出一個例外對象時在線程「main」的例外顯示的JVM的缺省例外處理打印消息。

休眠或停止休眠

在這一欄後面,我將向你介紹動畫??在一個表面上重複畫圖形,這稍微不同于完成一個運動畫面。要完成動畫,一個線程必須在它顯示兩個連續畫面時中止。調用Thread的靜態sleep(long millis)方法強迫一個線程中止millis毫秒。另一個線程可能中斷正在休眠的線程。假如這種事發生,正在休眠的線程將醒來並從sleep(long millis)方法擲出一個InterruptedException對象。結果,調用sleep(long millis)的代碼必須在一個try代碼塊中出現??或代碼方法必須在自己的throws子句中包括InterruptedException。

爲了示範sleep(long millis),我寫了一個CalcPI1應用程序。這個應用程序開始了一個新線程便于用一個數學運算法則計算數學常量pi的值。當新線程計算時,開始線程通過調用sleep(long millis)中止10毫秒。在開始線程醒後,它將打印pi的值,其中新線程存貯在變量pi中。表3給出了CalcPI1的源代碼:

表3. CalcPI1.java

// CalcPI1.java

class CalcPI1

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

try

{

Thread.sleep (10); //休眠10毫秒

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化爲0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

假如你運行這個程序,你將看到輸出如下(但也可能不一樣):

pi = -0.2146197014017295

完成計算PI

爲什麽輸出不正確呢?究竟,pi的值應近似等于3.14159。回答是:開始線程醒得太快了。在新線程剛開始計算pi時,開始線程就醒過來讀取pi的當前值並打印其值。我們可以通過將10毫秒延遲增加爲更長的值來進行補償。這一更長的值(不幸的是它是依靠于平台的)將給新線程一個機會在開始線程醒過來之前完成計算。(後面,你將學到一種不依靠平台的技術,它將防止開始線程醒來直到新線程完成。)

注重

線程同時提供一個sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因爲多數基于JVM的平台都不支持納秒級的分解度,JVM 線程處理代碼將納秒數字四舍五入成毫秒數字的近似值。假如一個平台不支持毫秒級的分解度,JVM 線程處理代碼將毫秒數字四舍五入成平台支持的最小級分解度的近似倍數。

它是死的還是活的?

當一個程序調用Thread的start()方法時,在一個新線程調用run()之前有一個時間段(爲了初始化)。run()返回後,在JVM清除線程之前有一段時間通過。JVM認爲線程立即激活優先于線程調用run(),在線程執行run()期間和run()返回後。在這時間間隔期間,Thread的isAlive()方法返回一個布爾真值。否則,方法返回一個假值。

isAlive()在一個線程需要在第一個線程能夠檢查其它線程的結果之前等待另一個線程完成其run()方法的情形下證實是有幫助的。實質上,那些需要等待的線程輸入一個while循環。當isAlive()爲其它線程返回真值時,等待線程調用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費更多的CPU循環)。一旦isAlive()返回假值,等待線程便檢查其它線程的結果。

你將在哪裏使用這樣的技術呢?對于起動器,一個CalcPI1的修改版本怎麽樣,在打印pi的值前開始線程在哪裏等待新線程的完成?表4的CalcPI2源代碼示範了這一技術:

表4. CalcPI2.java

// CalcPI2.java

class CalcPI2

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

while (mt.isAlive ())

try

{

Thread.sleep (10); //休眠10毫秒

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化成0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

CalcPI2的開始線程在10毫秒時間間隔休眠,直到mt.isAlive ()返回假值。當那些發生時,開始線程從它的while循環中退出並打印pi的內容。假如你運行這個程序,你將看到如下的輸出(但不一定一樣):

完成計算PI

pi = 3.1415726535897894

這不,現在看上去更精確了?

注重

一個線程可能對它自己調用isAlive() 方法。然而,這毫無意義,因爲isAlive()將一直返回真值。

合力

因爲while循環/isAlive()方法/sleep()方法技術證實是有用的,Sun將其打包進三個方法組成的一個組合裏:join(),join(long millis)和join(long millis, int nanos)。當當前線程想等待其它線程結束時,經由另一個線程的線程對象引用調用join()。相反,當它想其中任意線程等待其它線程結束或等待直到millis毫秒和nanos納秒組合通過時,當前線程調用join(long millis)或join(long millis, int nanos)。(作爲sleep()方法,JVM 線程處理代碼將對join(long millis)和join(long millis,int nanos)方法的參數值四舍五入。)表5的CalcPI3源代碼示範了一個對join()的調用:

表5. CalcPI3.java

// CalcPI3.java

class CalcPI3

{

public static void main (String [] args)

{

MyThread mt = new MyThread ();

mt.start ();

try

{

mt.join ();

}

catch (InterruptedException e)

{

}

System.out.println ("pi = " + mt.pi);

}

}

class MyThread extends Thread

{

boolean negative = true;

double pi; //缺省初始化成0.0

public void run ()

{

for (int i = 3; i

{

if (negative)

pi -= (1.0 / i);

else

pi += (1.0 / i);

negative = !negative;

}

pi += 1.0;

pi *= 4.0;

System.out.println ("Finished calculating PI");

}

}

CalcPI3的開始線程等待與MyThread對象有關被mt引用的線程結束。接著開始線程打印pi的值,其值與CalcPI2的輸出一樣。

警告

不要試圖將當前線程與其自身連接,因爲這樣當前線程將要永遠等待。

查詢活躍線程

在有些情形下,你可能想了解在你的程序中哪些線程是激活的。Thread支持一對方法幫助你完成這個任務: activeCount()和 enumerate(Thread [] thdarray)。但那些方法只工作在當前線程的線程組中。換句話說,那些方法只識別屬于當前線程的同一線程組的活躍線程。 (我將在以後的系列文章中討論線程組??一種組織機制。)

靜態activeCount()方法返回在當前線程的線程組中正在活躍運行的線程數量。一個程序利用這個方法的整數返回值設定一個Thread引用數組的大小。檢索那些引用,程序必須調用靜態enumerate(Thread [] thdarray)方法。這個方法的整數返回值確定Thread引用存貯在數組中的enumerate(Thread []thdarray)的總數。要看這些方法如何一起工作,請查看表6:

表6. Census.java

// Census.java

class Census

{

public static void main (String [] args)

{

Thread [] threads = new Thread [Thread.activeCount ()];

int n = Thread.enumerate (threads);

for (int i = 0; i

System.out.println (threads [i].toString ());

}

}

在運行時,這個程序會産生如下的輸出:

Thread[main,5,main]

輸出顯示一個線程,開始線程正在運行。左邊的main表示線程的名稱。5顯示線程的優先權,右邊的main表示線程的線程組。你也許很失望不能在輸出中看到任何系統線程,比如垃圾收集器線程。那種限制由Thread的enumerate(Thread [] thdarray) 方法産生,它僅詢問當前線程線程組的活躍線程。然而, ThreadGroup類包含多種enumerate()方法答應你捕捉對所有活躍線程的引用而不管線程組。在稍後的系列中,探討ThreadGroup時我將向你顯示如何列舉所有的引用。

警告

當重申一個數組時不要依靠activeCount()的返回值。假如你這樣做了,你的程序將冒擲出一個NullPointerException對象的風險。爲什麽呢?在調用activeCount()和enumerate(Thread [] thdarray)之間,一個或更多線程可能結束。結果, enumerate(Thread [] thdarray)能夠複制少數線程引用進它的數組。因此,僅考慮將activeCount()的返回值作爲數組可能大小的最大值。同樣,考慮將enumerate(Thread [] thdarray)的返回值作爲在一個程序對那種方法調用時活躍線程的數目。

反臭蟲

假如你的程序出現故障並且你懷疑問題出在線程,通過調用Thread的dumpStack()和toString()方法你能夠了解到線程的更多細節。靜態dumpStack()方法提供一個new Exception ("Stack trace").printStackTrace ()的封裝,打印一個追蹤當前線程的堆棧。toString()依據下面格式返回一個描述線程的名稱、優先權和線程組的字符串: Thread[thread-name,priority,thread-group]. (在稍後的系列中你將學到更多關于優先權的知識。)

技巧

在一些地方,這篇文章提到了當前線程的概念。假如你需要訪問描述當前線程的Thread對象,則調用Thread的靜態currentThread()方法。例:Thread current = Thread.currentThread ()。

等級系統

不是所有線程都被平等創建。它們被分成兩類:用戶和監督。一個用戶線程執行著對于程序用戶十分重要的工作,工作必須在程序結束前完成。相反,一個監督線程執行著後勤事務(比如垃圾收集)和其它可能不會對應用程序的主要工作作出貢獻但對于應用程序繼續它的主要工作卻非常必要的後台任務。和用戶線程不一樣,監督線程不需要在應用程序結束前完成。當一個應用程序的開始線程(它是一個用戶線程)結束時,JVM檢查是否還有其它用戶線程正在運行。假如有,JVM就會阻止應用程序結束。否則,JVM就會結束應用程序而不管監督線程是否正在運行。

當一個線程調用一個線程對象的start()方法時,新的已經開始的線程就是一個用戶線程。那是缺省的。要建立一個線程作爲監督線程,程序必須在調用start()前調用Thread的一個帶布爾真值參數的setDaemon(boolean isDaemon)方法。稍後,你可以通過調用Thread的isDaemon()方法檢查一個線程是否是監督線程。假如是監督線程那個方法返回一個布爾真值。

爲了讓你試試用戶和監督線程,我寫了一個UserDaemonThreadDemo:

表7. UserDaemonThreadDemo.java

// UserDaemonThreadDemo.java

class UserDaemonThreadDemo

{

public static void main (String [] args)

{

if (args.length == 0)

new MyThread ().start ();

else

{

MyThread mt = new MyThread ();

mt.setDaemon (true);

mt.start ();

}

try

{

Thread.sleep (100);

}

catch (InterruptedException e)

{

}

}

}

class MyThread extends Thread

{

public void run ()

{

System.out.println ("Daemon is " + isDaemon ());

while (true);

}

}

編譯了代碼後,通過Java2 SDK的java命令運行UserDaemonThreadDemo。假如你沒有使用命令行參數運行程序,例如java UserDaemonThreadDemo, new MyThread ().start ()執行。這段代碼片斷開始一個在進入一個無限循環前打印Daemon is false的用戶線程。(你必須按Ctrl-C或一個等價于結束一個無限循環的組合按鍵。)因爲新線程是一個用戶線程,應用程序在開始線程結束後仍保持運行。然而,假如你指定了至少一個命令行參數,例如java UserDaemonThreadDemo x,mt.setDaemon (true)執行並且新線程將是一個監督線程。結果,一旦開始線程從100毫秒休眠中醒來並結束,新的監督線程也將結束。

警告

假如線程開始執行後調用setDaemon(boolean isDaemon)方法,setDaemon(boolean isDaemon)方法將擲出一個IllegalThreadStateException對象。

Runnable

學習前面部份的例子後,你可能認爲引入多線程進入一個類總是要求你去擴展Thread並將你的子類重載Thread's run()方法。然而那並不總是一種選擇。Java對繼續的強制執行禁止一個類擴展兩個或更多個超類。結果,假如一個類擴展了一個無線程類,那個類就不能擴展Thread. 假使限制,怎樣才可能將多線程引入一個已經擴展了其它類的類?幸運的是, Java的設計者已經意識到不可能創建Thread子類的情形總會發生的。這導致産生java.lang.Runnable接口和帶Runnable參數的Thread構造器,如Thread(Runnable target)。

Runnable接口聲明了一個單獨方法署名:void run()。這個署名和Thread的run()方法署名一樣並作爲線程的執行入口服務。因爲Runnable是一個接口,任何類都能通過將一個implements子句包含進類頭和提供一個適當的run()方法實現接口。在執行時間,程序代碼能從那個類創建一個對象或runnable並將runnable的引用傳遞給一個適當的Thread構造器。構造器和Thread對象一起存貯這個引用並確保一個新線程在調用Thread對象的start()方法後調用runnable的run()方法。示範如表8:

表8.RunnableDemo.java

// RunnableDemo.java

class RunnableDemo

{

public static void main (String [] args)

{

Rectangle r = new Rectangle (5, 6);

r.draw ();

//用隨機選擇的寬度和高度畫不同的長方形

new Rectangle ();

}

}

abstract class Shape

{

abstract void draw ();

}

class Rectangle extends Shape implements Runnable

{

private int w, h;

Rectangle ()

{

//創建一個綁定這個runnable的新Thread對象並開始一個將調用這個runnable的

//run()方法的線程

new Thread (this).start ();

}

Rectangle (int w, int h)

{

if (w

throw new IllegalArgumentException ("w value " + w + "

if (h

throw new IllegalArgumentException ("h value " + h + "

this.w = w;

this.h = h;

}

void draw ()

{

for (int c = 0; c

System.out.print ('*');

System.out.print ('

 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
  用Java線程獲取優異性能(I) 摘要 用戶期望程序能展現優異的性能。爲了滿足這個期望,你的程序經常使用到線程。在這篇文章中我們開始練習使用線程。你將學習到線程、線程類及Runnable。 用戶不喜歡反應遲鈍的軟件。當用戶單擊一個鼠標時,他們希望程序立即回應他們的請求,即使程序正處于費時的運行之中,比如爲一篇很長的文檔重編頁碼或等待一個網絡操作的完成。對用戶響應很慢的程序其性能拙劣。爲提高程序性能,開發者一般使用線程。 這篇文章是探索線程的第一部份。雖然你可能認爲線程是一種難于把握的事物,但我打算向你顯示線程是易于理解的。在這篇文章中,我將向你介紹線程和線程類,以及討論Runnable。此外,在後面的文章中,我將探索同步(通過鎖),同步的問題(比如死鎖),等待/通知機制,時序安排(有優先權和沒有優先權),線程中斷,計時器,揮發性,線程組和線程本地變量。 閱讀關于線程設計的整個系列: ?第1部份:介紹線程和線程類,以及Runnable ?第2部份:使用同步使線程串行化訪問要害代碼部份 注重 這篇文章及其應用程序的三個相關線程練習與applets不同。然而,我在應用程序中介紹的多數應用到applets。主要不同的是:爲了安全的原因,不是所有的線程操作都可以放到一個applet中(我將在以後的文章中討論applets)。 什麽是線程? 線程的概念並不難于把握:它是程序代碼的一個獨立的執行通道。當多個線程執行時,經由相同代碼的一個線程的通道通常與其它的不同。例如,假設一個線程執行一段相當于一個if-else語句的if部分的字節代碼時,而另一個線程正執行相當于else部分的字節代碼。JVM怎樣保持對于每一個線程執行的跟蹤呢?JVM給每一個線程它自己的方法調用堆棧。另外跟蹤當前指令字節代碼,方法堆棧跟蹤本地變量,JVM傳遞給一個方法的參數,以及方法的返回值。 當多個線程在同一個程序中執行字節代碼序列時,這種行爲叫作多線程。多線程在多方面有利于程序: ?當執行其它任務時多線程GUI(圖形用戶界面)程序仍能保持對用戶的響應,比如重編頁碼或打印一個文檔。 ?帶線程的程序一般比它們沒有帶線程的副本程序完成得快。這尤其表現在線程運行在一個多處理器機器上,在這裏每一個線程都有它自己的處理器。 Java通過java.lang.Thread類完成多線程。每一個線程對象描述一個單獨的執行線程。那些運行發生在線程的run()方法中。因爲缺省的run()方法什麽都不做,你必須創建Thread子類並重載run()以完成有用的工作。練習列表1中領略一個在Thread中的線程及多線程: 列表1. ThreadDemo.java // ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row { for (int i = 0; i System.out.print ('*'); System.out.print (' '); } } } 列表1顯示了一個由類ThreadDemo和MyThread組成的應用程序的源代碼。類ThreadDemo通過創建一個MyThread對象驅動應用程序,開始一個與其對象相關的線程並執行一段打印一個正方形表的代碼。相反, MyThread重載Thread的run()方法打印(通過標准輸入流)一個由星形符號組成的直角三角形。 當你鍵入java ThreadDemo運行應用程序時, JVM創建一個運行main()方法的開始線程。通過執行mt.start (),開始線程告訴JVM創建一個執行包含MyThread對象的run()方法的字節代碼指令的第二個線程。當start()方法返回時,開始線程循環執行打印一個正方形表,此時另一個新線程執行run()方法打印直角三角形。 輸出會象什麽樣呢?運行ThreadDemo就可以看到。你將注重到每一個線程的輸出與其它線程的輸出相互交替。這樣的結果是因爲兩個線程將它們的輸出都發送到了同樣的標准輸出流。 注重 多數(不是所有)JVM設備使用下層平台的線程性能。因爲那些性能是平台特有的,你的多線程程序的輸出順序可能與一些人的其他輸出的順序不一樣。這種不同是由于時序的安排,我將在這一系列的稍後探討這一話題。 線程類 要精通寫多線程代碼,你必須首先理解創建Thread類的多種方法。這部份將探討這些方法。明確地說,你將學到開始線程的方法,命名線程,使線程休眠,決定一個線程是否激活,將一個線程與另一個線程相聯,和在當前線程的線程組及子組中列舉所有激活的線程。我也會討論線程調試輔助程序及用戶線程與監督線程的對比。 我將在以後的文章中介紹線程方法的余下部份,Sun不贊成的方法除外。 警告 Sun有一些不贊成的線程方法種類,比如suspend()和resume(),因爲它們能鎖住你的程序或破壞對象。所以,你不必在你的代碼中調用它們。考慮到針對這些方法工作區的SDK文件,在這篇文章中我沒有包含這些方法。 構造線程 Thread有八個構造器。最簡單的是: ?Thread(),用缺省名稱創建一個Thread對象 ?Thread(String name),用指定的name參數的名稱創建一個Thread對象 下一個最簡單的構造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable參數之外,這些構造器與前述的構造器一樣。不同的是:Runnable參數識別提供run()方法的線程之外的對象。(你將在這篇文章稍後學到Runnable。)最後幾個構造器是Thread(String name),Thread(Runnable target),和Thread(Runnable target, String name)。然而,最後的構造器包含了一個爲了組織意圖的ThreadGroup參數。 最後四個構造器之一,Thread(ThreadGroup group, Runnable target, String name, long stackSize),令人感愛好的是它能夠讓你指定想要的線程方法調用堆棧的大小。能夠指定大小將證實在使用遞歸方法(一種爲何一個方法不斷重複調用自身的技術)美麗地解決一些問題的程序中是十分有幫助的。通過明確地設置堆棧大小,你有時能夠預防StackOverflowErrors。然而,太大將導致OutOfMemoryErrors。同樣,Sun將方法調用堆棧的大小看作平台依靠。依靠平台,方法調用堆棧的大小可能改變。因此,在寫調用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代碼前仔細考慮你的程序分枝。 開始你的運載工具 線程類似于運載工具:它們將程序從開始移動到結束。Thread 和Thread子類對象不是線程。它們描述一個線程的屬性,比如名稱和包含線程執行的代碼(經由一個run()方法)。當一個新線程執行run()時,另一個線程正調用Thread或其子類對象的start()方法。例如,要開始第二個線程,應用程序的開始線程?它執行main()?調用start()。作爲響應,JVM和平台一起工作的線程操作代碼確保線程正確地初始化並調用Thread或其子類對象的run()方法。 一旦start()完成,多重線程便運行。因爲我們趨向于在一種線性的方式中思維,我們常發現當兩個或更多線程正運行時理解並發(同時)行爲是困難的。因此,你應該看看顯示與時間對比一個線程正在哪裏執行(它的位置)的圖表。下圖就是這樣一個圖表。 與時間對比一個開始線程和一個新建線程執行位置的行爲 圖表顯示了幾個重要的時間段: ?開始線程的初始化 ?線程開始執行main()瞬間 ?線程開始執行start()的瞬間 ?start()創建一個新線程並返回main()的瞬間 ?新線程的初始化 ?新線程開始執行run()的瞬間 ?每個線程結束的不同瞬間 注重新線程的初始化,它對run()的執行,和它的結束都與開始線程的執行同時發生。 警告 一個線程調用start()後,在run()方法退出前並發調用那方法將導致start()擲出一個java.lang.IllegalThreadStateException對象。 怎樣使用名稱 在一個調試會話期間,使用用戶友好方式從另一個線程區別其中一個線程證實是有幫助的。要區分其中一個線程,Java給一個線程取一個名稱。Thread缺省的名稱是一個短線連字符和一個零開始的數字符號。你可以接受Java的缺省線程名稱或選擇使用你自己的。爲了能夠自定義名稱,Thread提供帶有name參數和一個setName(String name)方法的構造器。Thread也提供一個getName()方法返回當前名稱。表2顯示了怎樣通過Thread(String name)創建一個自定義名稱和通過在run()方法中調用getName()檢索當前名稱: 表2.NameThatThread.java // NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { //編譯器創建等價于super()的字節代碼 } MyThread (String name) { super (name); //將名稱傳遞給Thread超類 } public void run () { System.out.println ("My name is: " + getName ()); } } 你能夠在命令行向MyThread傳遞一個可選的name參數。例如,java NameThatThread X 建立X作爲線程的名稱。假如你指定一個名稱失敗,你將看到下面的輸出: My name is: Thread-1 假如你喜歡,你能夠在MyThread(String name)構造器中將super(name)調用改變成setName(String name)調用??作爲setName(name)後一種方法調用達到同樣建立線程名稱的目的??作爲super(name)我作爲練習保留給你們。 注重 Java主要將名稱指派給運行main() 方法的線程,開始線程。你非凡要看看當開始線程擲出一個例外對象時在線程「main」的例外顯示的JVM的缺省例外處理打印消息。 休眠或停止休眠 在這一欄後面,我將向你介紹動畫??在一個表面上重複畫圖形,這稍微不同于完成一個運動畫面。要完成動畫,一個線程必須在它顯示兩個連續畫面時中止。調用Thread的靜態sleep(long millis)方法強迫一個線程中止millis毫秒。另一個線程可能中斷正在休眠的線程。假如這種事發生,正在休眠的線程將醒來並從sleep(long millis)方法擲出一個InterruptedException對象。結果,調用sleep(long millis)的代碼必須在一個try代碼塊中出現??或代碼方法必須在自己的throws子句中包括InterruptedException。 爲了示範sleep(long millis),我寫了一個CalcPI1應用程序。這個應用程序開始了一個新線程便于用一個數學運算法則計算數學常量pi的值。當新線程計算時,開始線程通過調用sleep(long millis)中止10毫秒。在開始線程醒後,它將打印pi的值,其中新線程存貯在變量pi中。表3給出了CalcPI1的源代碼: 表3. CalcPI1.java // CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); //休眠10毫秒 } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化爲0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } 假如你運行這個程序,你將看到輸出如下(但也可能不一樣): pi = -0.2146197014017295 完成計算PI 爲什麽輸出不正確呢?究竟,pi的值應近似等于3.14159。回答是:開始線程醒得太快了。在新線程剛開始計算pi時,開始線程就醒過來讀取pi的當前值並打印其值。我們可以通過將10毫秒延遲增加爲更長的值來進行補償。這一更長的值(不幸的是它是依靠于平台的)將給新線程一個機會在開始線程醒過來之前完成計算。(後面,你將學到一種不依靠平台的技術,它將防止開始線程醒來直到新線程完成。) 注重 線程同時提供一個sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因爲多數基于JVM的平台都不支持納秒級的分解度,JVM 線程處理代碼將納秒數字四舍五入成毫秒數字的近似值。假如一個平台不支持毫秒級的分解度,JVM 線程處理代碼將毫秒數字四舍五入成平台支持的最小級分解度的近似倍數。 它是死的還是活的? 當一個程序調用Thread的start()方法時,在一個新線程調用run()之前有一個時間段(爲了初始化)。run()返回後,在JVM清除線程之前有一段時間通過。JVM認爲線程立即激活優先于線程調用run(),在線程執行run()期間和run()返回後。在這時間間隔期間,Thread的isAlive()方法返回一個布爾真值。否則,方法返回一個假值。 isAlive()在一個線程需要在第一個線程能夠檢查其它線程的結果之前等待另一個線程完成其run()方法的情形下證實是有幫助的。實質上,那些需要等待的線程輸入一個while循環。當isAlive()爲其它線程返回真值時,等待線程調用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費更多的CPU循環)。一旦isAlive()返回假值,等待線程便檢查其它線程的結果。 你將在哪裏使用這樣的技術呢?對于起動器,一個CalcPI1的修改版本怎麽樣,在打印pi的值前開始線程在哪裏等待新線程的完成?表4的CalcPI2源代碼示範了這一技術: 表4. CalcPI2.java // CalcPI2.java class CalcPI2 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); while (mt.isAlive ()) try { Thread.sleep (10); //休眠10毫秒 } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化成0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } CalcPI2的開始線程在10毫秒時間間隔休眠,直到mt.isAlive ()返回假值。當那些發生時,開始線程從它的while循環中退出並打印pi的內容。假如你運行這個程序,你將看到如下的輸出(但不一定一樣): 完成計算PI pi = 3.1415726535897894 這不,現在看上去更精確了? 注重 一個線程可能對它自己調用isAlive() 方法。然而,這毫無意義,因爲isAlive()將一直返回真值。 合力 因爲while循環/isAlive()方法/sleep()方法技術證實是有用的,Sun將其打包進三個方法組成的一個組合裏:join(),join(long millis)和join(long millis, int nanos)。當當前線程想等待其它線程結束時,經由另一個線程的線程對象引用調用join()。相反,當它想其中任意線程等待其它線程結束或等待直到millis毫秒和nanos納秒組合通過時,當前線程調用join(long millis)或join(long millis, int nanos)。(作爲sleep()方法,JVM 線程處理代碼將對join(long millis)和join(long millis,int nanos)方法的參數值四舍五入。)表5的CalcPI3源代碼示範了一個對join()的調用: 表5. CalcPI3.java // CalcPI3.java class CalcPI3 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { mt.join (); } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化成0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } CalcPI3的開始線程等待與MyThread對象有關被mt引用的線程結束。接著開始線程打印pi的值,其值與CalcPI2的輸出一樣。 警告 不要試圖將當前線程與其自身連接,因爲這樣當前線程將要永遠等待。 怎樣使用名稱 在一個調試會話期間,使用用戶友好方式從另一個線程區別其中一個線程證實是有幫助的。要區分其中一個線程,Java給一個線程取一個名稱。Thread缺省的名稱是一個短線連字符和一個零開始的數字符號。你可以接受Java的缺省線程名稱或選擇使用你自己的。爲了能夠自定義名稱,Thread提供帶有name參數和一個setName(String name)方法的構造器。Thread也提供一個getName()方法返回當前名稱。表2顯示了怎樣通過Thread(String name)創建一個自定義名稱和通過在run()方法中調用getName()檢索當前名稱: 表2.NameThatThread.java // NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { //編譯器創建等價于super()的字節代碼 } MyThread (String name) { super (name); //將名稱傳遞給Thread超類 } public void run () { System.out.println ("My name is: " + getName ()); } } 你能夠在命令行向MyThread傳遞一個可選的name參數。例如,java NameThatThread X 建立X作爲線程的名稱。假如你指定一個名稱失敗,你將看到下面的輸出: My name is: Thread-1 假如你喜歡,你能夠在MyThread(String name)構造器中將super(name)調用改變成setName(String name)調用??作爲setName(name)後一種方法調用達到同樣建立線程名稱的目的??作爲super(name)我作爲練習保留給你們。 注重 Java主要將名稱指派給運行main() 方法的線程,開始線程。你非凡要看看當開始線程擲出一個例外對象時在線程「main」的例外顯示的JVM的缺省例外處理打印消息。 休眠或停止休眠 在這一欄後面,我將向你介紹動畫??在一個表面上重複畫圖形,這稍微不同于完成一個運動畫面。要完成動畫,一個線程必須在它顯示兩個連續畫面時中止。調用Thread的靜態sleep(long millis)方法強迫一個線程中止millis毫秒。另一個線程可能中斷正在休眠的線程。假如這種事發生,正在休眠的線程將醒來並從sleep(long millis)方法擲出一個InterruptedException對象。結果,調用sleep(long millis)的代碼必須在一個try代碼塊中出現??或代碼方法必須在自己的throws子句中包括InterruptedException。 爲了示範sleep(long millis),我寫了一個CalcPI1應用程序。這個應用程序開始了一個新線程便于用一個數學運算法則計算數學常量pi的值。當新線程計算時,開始線程通過調用sleep(long millis)中止10毫秒。在開始線程醒後,它將打印pi的值,其中新線程存貯在變量pi中。表3給出了CalcPI1的源代碼: 表3. CalcPI1.java // CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); //休眠10毫秒 } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化爲0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } 假如你運行這個程序,你將看到輸出如下(但也可能不一樣): pi = -0.2146197014017295 完成計算PI 爲什麽輸出不正確呢?究竟,pi的值應近似等于3.14159。回答是:開始線程醒得太快了。在新線程剛開始計算pi時,開始線程就醒過來讀取pi的當前值並打印其值。我們可以通過將10毫秒延遲增加爲更長的值來進行補償。這一更長的值(不幸的是它是依靠于平台的)將給新線程一個機會在開始線程醒過來之前完成計算。(後面,你將學到一種不依靠平台的技術,它將防止開始線程醒來直到新線程完成。) 注重 線程同時提供一個sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因爲多數基于JVM的平台都不支持納秒級的分解度,JVM 線程處理代碼將納秒數字四舍五入成毫秒數字的近似值。假如一個平台不支持毫秒級的分解度,JVM 線程處理代碼將毫秒數字四舍五入成平台支持的最小級分解度的近似倍數。 它是死的還是活的? 當一個程序調用Thread的start()方法時,在一個新線程調用run()之前有一個時間段(爲了初始化)。run()返回後,在JVM清除線程之前有一段時間通過。JVM認爲線程立即激活優先于線程調用run(),在線程執行run()期間和run()返回後。在這時間間隔期間,Thread的isAlive()方法返回一個布爾真值。否則,方法返回一個假值。 isAlive()在一個線程需要在第一個線程能夠檢查其它線程的結果之前等待另一個線程完成其run()方法的情形下證實是有幫助的。實質上,那些需要等待的線程輸入一個while循環。當isAlive()爲其它線程返回真值時,等待線程調用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費更多的CPU循環)。一旦isAlive()返回假值,等待線程便檢查其它線程的結果。 你將在哪裏使用這樣的技術呢?對于起動器,一個CalcPI1的修改版本怎麽樣,在打印pi的值前開始線程在哪裏等待新線程的完成?表4的CalcPI2源代碼示範了這一技術: 表4. CalcPI2.java // CalcPI2.java class CalcPI2 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); while (mt.isAlive ()) try { Thread.sleep (10); //休眠10毫秒 } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化成0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } CalcPI2的開始線程在10毫秒時間間隔休眠,直到mt.isAlive ()返回假值。當那些發生時,開始線程從它的while循環中退出並打印pi的內容。假如你運行這個程序,你將看到如下的輸出(但不一定一樣): 完成計算PI pi = 3.1415726535897894 這不,現在看上去更精確了? 注重 一個線程可能對它自己調用isAlive() 方法。然而,這毫無意義,因爲isAlive()將一直返回真值。 合力 因爲while循環/isAlive()方法/sleep()方法技術證實是有用的,Sun將其打包進三個方法組成的一個組合裏:join(),join(long millis)和join(long millis, int nanos)。當當前線程想等待其它線程結束時,經由另一個線程的線程對象引用調用join()。相反,當它想其中任意線程等待其它線程結束或等待直到millis毫秒和nanos納秒組合通過時,當前線程調用join(long millis)或join(long millis, int nanos)。(作爲sleep()方法,JVM 線程處理代碼將對join(long millis)和join(long millis,int nanos)方法的參數值四舍五入。)表5的CalcPI3源代碼示範了一個對join()的調用: 表5. CalcPI3.java // CalcPI3.java class CalcPI3 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { mt.join (); } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; //缺省初始化成0.0 public void run () { for (int i = 3; i { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } } CalcPI3的開始線程等待與MyThread對象有關被mt引用的線程結束。接著開始線程打印pi的值,其值與CalcPI2的輸出一樣。 警告 不要試圖將當前線程與其自身連接,因爲這樣當前線程將要永遠等待。 查詢活躍線程 在有些情形下,你可能想了解在你的程序中哪些線程是激活的。Thread支持一對方法幫助你完成這個任務: activeCount()和 enumerate(Thread [] thdarray)。但那些方法只工作在當前線程的線程組中。換句話說,那些方法只識別屬于當前線程的同一線程組的活躍線程。 (我將在以後的系列文章中討論線程組??一種組織機制。) 靜態activeCount()方法返回在當前線程的線程組中正在活躍運行的線程數量。一個程序利用這個方法的整數返回值設定一個Thread引用數組的大小。檢索那些引用,程序必須調用靜態enumerate(Thread [] thdarray)方法。這個方法的整數返回值確定Thread引用存貯在數組中的enumerate(Thread []thdarray)的總數。要看這些方法如何一起工作,請查看表6: 表6. Census.java // Census.java class Census { public static void main (String [] args) { Thread [] threads = new Thread [Thread.activeCount ()]; int n = Thread.enumerate (threads); for (int i = 0; i System.out.println (threads [i].toString ()); } } 在運行時,這個程序會産生如下的輸出: Thread[main,5,main] 輸出顯示一個線程,開始線程正在運行。左邊的main表示線程的名稱。5顯示線程的優先權,右邊的main表示線程的線程組。你也許很失望不能在輸出中看到任何系統線程,比如垃圾收集器線程。那種限制由Thread的enumerate(Thread [] thdarray) 方法産生,它僅詢問當前線程線程組的活躍線程。然而, ThreadGroup類包含多種enumerate()方法答應你捕捉對所有活躍線程的引用而不管線程組。在稍後的系列中,探討ThreadGroup時我將向你顯示如何列舉所有的引用。 警告 當重申一個數組時不要依靠activeCount()的返回值。假如你這樣做了,你的程序將冒擲出一個NullPointerException對象的風險。爲什麽呢?在調用activeCount()和enumerate(Thread [] thdarray)之間,一個或更多線程可能結束。結果, enumerate(Thread [] thdarray)能夠複制少數線程引用進它的數組。因此,僅考慮將activeCount()的返回值作爲數組可能大小的最大值。同樣,考慮將enumerate(Thread [] thdarray)的返回值作爲在一個程序對那種方法調用時活躍線程的數目。 反臭蟲 假如你的程序出現故障並且你懷疑問題出在線程,通過調用Thread的dumpStack()和toString()方法你能夠了解到線程的更多細節。靜態dumpStack()方法提供一個new Exception ("Stack trace").printStackTrace ()的封裝,打印一個追蹤當前線程的堆棧。toString()依據下面格式返回一個描述線程的名稱、優先權和線程組的字符串: Thread[thread-name,priority,thread-group]. (在稍後的系列中你將學到更多關于優先權的知識。) 技巧 在一些地方,這篇文章提到了當前線程的概念。假如你需要訪問描述當前線程的Thread對象,則調用Thread的靜態currentThread()方法。例:Thread current = Thread.currentThread ()。 等級系統 不是所有線程都被平等創建。它們被分成兩類:用戶和監督。一個用戶線程執行著對于程序用戶十分重要的工作,工作必須在程序結束前完成。相反,一個監督線程執行著後勤事務(比如垃圾收集)和其它可能不會對應用程序的主要工作作出貢獻但對于應用程序繼續它的主要工作卻非常必要的後台任務。和用戶線程不一樣,監督線程不需要在應用程序結束前完成。當一個應用程序的開始線程(它是一個用戶線程)結束時,JVM檢查是否還有其它用戶線程正在運行。假如有,JVM就會阻止應用程序結束。否則,JVM就會結束應用程序而不管監督線程是否正在運行。 當一個線程調用一個線程對象的start()方法時,新的已經開始的線程就是一個用戶線程。那是缺省的。要建立一個線程作爲監督線程,程序必須在調用start()前調用Thread的一個帶布爾真值參數的setDaemon(boolean isDaemon)方法。稍後,你可以通過調用Thread的isDaemon()方法檢查一個線程是否是監督線程。假如是監督線程那個方法返回一個布爾真值。 爲了讓你試試用戶和監督線程,我寫了一個UserDaemonThreadDemo: 表7. UserDaemonThreadDemo.java // UserDaemonThreadDemo.java class UserDaemonThreadDemo { public static void main (String [] args) { if (args.length == 0) new MyThread ().start (); else { MyThread mt = new MyThread (); mt.setDaemon (true); mt.start (); } try { Thread.sleep (100); } catch (InterruptedException e) { } } } class MyThread extends Thread { public void run () { System.out.println ("Daemon is " + isDaemon ()); while (true); } } 編譯了代碼後,通過Java2 SDK的java命令運行UserDaemonThreadDemo。假如你沒有使用命令行參數運行程序,例如java UserDaemonThreadDemo, new MyThread ().start ()執行。這段代碼片斷開始一個在進入一個無限循環前打印Daemon is false的用戶線程。(你必須按Ctrl-C或一個等價于結束一個無限循環的組合按鍵。)因爲新線程是一個用戶線程,應用程序在開始線程結束後仍保持運行。然而,假如你指定了至少一個命令行參數,例如java UserDaemonThreadDemo x,mt.setDaemon (true)執行並且新線程將是一個監督線程。結果,一旦開始線程從100毫秒休眠中醒來並結束,新的監督線程也將結束。 警告 假如線程開始執行後調用setDaemon(boolean isDaemon)方法,setDaemon(boolean isDaemon)方法將擲出一個IllegalThreadStateException對象。 Runnable 學習前面部份的例子後,你可能認爲引入多線程進入一個類總是要求你去擴展Thread並將你的子類重載Thread's run()方法。然而那並不總是一種選擇。Java對繼續的強制執行禁止一個類擴展兩個或更多個超類。結果,假如一個類擴展了一個無線程類,那個類就不能擴展Thread. 假使限制,怎樣才可能將多線程引入一個已經擴展了其它類的類?幸運的是, Java的設計者已經意識到不可能創建Thread子類的情形總會發生的。這導致産生java.lang.Runnable接口和帶Runnable參數的Thread構造器,如Thread(Runnable target)。 Runnable接口聲明了一個單獨方法署名:void run()。這個署名和Thread的run()方法署名一樣並作爲線程的執行入口服務。因爲Runnable是一個接口,任何類都能通過將一個implements子句包含進類頭和提供一個適當的run()方法實現接口。在執行時間,程序代碼能從那個類創建一個對象或runnable並將runnable的引用傳遞給一個適當的Thread構造器。構造器和Thread對象一起存貯這個引用並確保一個新線程在調用Thread對象的start()方法後調用runnable的run()方法。示範如表8: 表8.RunnableDemo.java // RunnableDemo.java class RunnableDemo { public static void main (String [] args) { Rectangle r = new Rectangle (5, 6); r.draw (); //用隨機選擇的寬度和高度畫不同的長方形 new Rectangle (); } } abstract class Shape { abstract void draw (); } class Rectangle extends Shape implements Runnable { private int w, h; Rectangle () { //創建一個綁定這個runnable的新Thread對象並開始一個將調用這個runnable的 //run()方法的線程 new Thread (this).start (); } Rectangle (int w, int h) { if (w throw new IllegalArgumentException ("w value " + w + " if (h throw new IllegalArgumentException ("h value " + h + " this.w = w; this.h = h; } void draw () { for (int c = 0; c System.out.print ('*'); System.out.print ('
󰈣󰈤
王朝萬家燈火計劃
期待原創作者加盟
 
 
 
>>返回首頁<<
 
 
 
 
 
 熱帖排行
 
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有