積累,創(chuàng)造,分享!

          BlogJava 首頁(yè) 新隨筆 聯(lián)系 聚合 管理
            25 Posts :: 13 Stories :: 26 Comments :: 0 Trackbacks
          摘要
          用戶期望程序能展現(xiàn)優(yōu)異的性能。為了滿足這個(gè)期望,你的程序常常使用到線程。在這篇文章中我們開(kāi)始練習(xí)使用線程。你將學(xué)習(xí)到線程、線程類及Runnable。

          用戶不喜歡反應(yīng)遲鈍的軟件。當(dāng)用戶單擊一個(gè)鼠標(biāo)時(shí),他們希望程序立即回應(yīng)他們的請(qǐng)求,即使程序正處于費(fèi)時(shí)的運(yùn)行之中,比如為一篇很長(zhǎng)的文檔重編頁(yè)碼或等待一個(gè)網(wǎng)絡(luò)操作的完成。對(duì)用戶響應(yīng)很慢的程序其性能拙劣。為提高程序性能,開(kāi)發(fā)者一般使用線程。
          這篇文章是探索線程的第一部份。雖然你可能認(rèn)為線程是一種難于掌握的事物,但我打算向你顯示線程是易于理解的。在這篇文章中,我將向你介紹線程和線程類,以及討論Runnable。此外,在后面的文章中,我將探索同步(通過(guò)鎖),同步的問(wèn)題(比如死鎖),等待/通知機(jī)制,時(shí)序安排(有優(yōu)先權(quán)和沒(méi)有優(yōu)先權(quán)),線程中斷,計(jì)時(shí)器,揮發(fā)性,線程組和線程本地變量。
          閱讀關(guān)于線程設(shè)計(jì)的整個(gè)系列:
          ·第1部份:介紹線程和線程類,以及Runnable
          ·第2部份:使用同步使線程串行化訪問(wèn)關(guān)鍵代碼部份
          注意
          這篇文章及其應(yīng)用程序的三個(gè)相關(guān)線程練習(xí)與applets不同。然而,我在應(yīng)用程序中介紹的多數(shù)應(yīng)用到applets。主要不同的是:為了安全的原因,不是所有的線程操作都可以放到一個(gè)applet中(我將在以后的文章中討論applets)。
          什么是線程?
          線程的概念并不難于掌握:它是程序代碼的一個(gè)獨(dú)立的執(zhí)行通道。當(dāng)多個(gè)線程執(zhí)行時(shí),經(jīng)由相同代碼的一個(gè)線程的通道通常與其它的不同。例如,假設(shè)一個(gè)線程執(zhí)行一段相當(dāng)于一個(gè)if-else語(yǔ)句的if部分的字節(jié)代碼時(shí),而另一個(gè)線程正執(zhí)行相當(dāng)于else部分的字節(jié)代碼。JVM怎樣保持對(duì)于每一個(gè)線程執(zhí)行的跟蹤呢?JVM給每一個(gè)線程它自己的方法調(diào)用堆棧。另外跟蹤當(dāng)前指令字節(jié)代碼,方法堆棧跟蹤本地變量,JVM傳遞給一個(gè)方法的參數(shù),以及方法的返回值。
          當(dāng)多個(gè)線程在同一個(gè)程序中執(zhí)行字節(jié)代碼序列時(shí),這種行為叫作多線程。多線程在多方面有利于程序:
          ·當(dāng)執(zhí)行其它任務(wù)時(shí)多線程GUI(圖形用戶界面)程序仍能保持對(duì)用戶的響應(yīng),比如重編頁(yè)碼或打印一個(gè)文檔。
          ·帶線程的程序一般比它們沒(méi)有帶線程的副本程序完成得快。這尤其表現(xiàn)在線程運(yùn)行在一個(gè)多處理器機(jī)器上,在這里每一個(gè)線程都有它自己的處理器。
          Java通過(guò)java.lang.Thread類完成多線程。每一個(gè)線程對(duì)象描述一個(gè)單獨(dú)的執(zhí)行線程。那些運(yùn)行發(fā)生在線程的run()方法中。因?yàn)槿笔〉膔un()方法什么都不做,你必須創(chuàng)建Thread子類并重載run()以完成有用的工作。練習(xí)列表1中領(lǐng)略一個(gè)在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 < 50; i++)
          System.out.println ("i = " + i + ", i * i = " + i * i);
          }
          }
          class MyThread extends Thread
          {
          public void run ()
          {
          for (int count = 1, row = 1; row < 20; row++, count++)
          {
          for (int i = 0; i < count; i++)
          System.out.print ('*');
          System.out.print ('\n');
          }
          }
          }
          列表1顯示了一個(gè)由類ThreadDemo和MyThread組成的應(yīng)用程序的源代碼。類ThreadDemo通過(guò)創(chuàng)建一個(gè)MyThread對(duì)象驅(qū)動(dòng)應(yīng)用程序,開(kāi)始一個(gè)與其對(duì)象相關(guān)的線程并執(zhí)行一段打印一個(gè)正方形表的代碼。相反, MyThread重載Thread的run()方法打印(通過(guò)標(biāo)準(zhǔn)輸入流)一個(gè)由星形符號(hào)組成的直角三角形。
          當(dāng)你鍵入java ThreadDemo運(yùn)行應(yīng)用程序時(shí), JVM創(chuàng)建一個(gè)運(yùn)行main()方法的開(kāi)始線程。通過(guò)執(zhí)行mt.start (),開(kāi)始線程告訴JVM創(chuàng)建一個(gè)執(zhí)行包含MyThread對(duì)象的run()方法的字節(jié)代碼指令的第二個(gè)線程。當(dāng)start()方法返回時(shí),開(kāi)始線程循環(huán)執(zhí)行打印一個(gè)正方形表,此時(shí)另一個(gè)新線程執(zhí)行run()方法打印直角三角形。
          輸出會(huì)象什么樣呢?運(yùn)行ThreadDemo就可以看到。你將注意到每一個(gè)線程的輸出與其它線程的輸出相互交替。這樣的結(jié)果是因?yàn)閮蓚€(gè)線程將它們的輸出都發(fā)送到了同樣的標(biāo)準(zhǔn)輸出流。
          注意
          多數(shù)(不是所有)JVM設(shè)備使用下層平臺(tái)的線程性能。因?yàn)槟切┬阅苁瞧脚_(tái)特有的,你的多線程程序的輸出順序可能與一些人的其他輸出的順序不一樣。這種不同是由于時(shí)序的安排,我將在這一系列的稍后探討這一話題。
          線程類
          要精通寫多線程代碼,你必須首先理解創(chuàng)建Thread類的多種方法。這部份將探討這些方法。明確地說(shuō),你將學(xué)到開(kāi)始線程的方法,命名線程,使線程休眠,決定一個(gè)線程是否激活,將一個(gè)線程與另一個(gè)線程相聯(lián),和在當(dāng)前線程的線程組及子組中列舉所有激活的線程。我也會(huì)討論線程調(diào)試輔助程序及用戶線程與監(jiān)督線程的對(duì)比。
          我將在以后的文章中介紹線程方法的余下部份,Sun不贊成的方法除外。
          警告
          Sun有一些不贊成的線程方法種類,比如suspend()和resume(),因?yàn)樗鼈兡苕i住你的程序或破壞對(duì)象。所以,你不必在你的代碼中調(diào)用它們。考慮到針對(duì)這些方法工作區(qū)的SDK文件,在這篇文章中我沒(méi)有包含這些方法。
          構(gòu)造線程
          Thread有八個(gè)構(gòu)造器。最簡(jiǎn)單的是:
          ·Thread(),用缺省名稱創(chuàng)建一個(gè)Thread對(duì)象
          ·Thread(String name),用指定的name參數(shù)的名稱創(chuàng)建一個(gè)Thread對(duì)象
          下一個(gè)最簡(jiǎn)單的構(gòu)造器是Thread(Runnable target)和Thread(Runnable target, String name)。 除Runnable參數(shù)之外,這些構(gòu)造器與前述的構(gòu)造器一樣。不同的是:Runnable參數(shù)識(shí)別提供run()方法的線程之外的對(duì)象。(你將在這篇文章稍后學(xué)到Runnable。)最后幾個(gè)構(gòu)造器是Thread(String name),Thread(Runnable target),和Thread(Runnable target, String name)。然而,最后的構(gòu)造器包含了一個(gè)為了組織意圖的ThreadGroup參數(shù)。
          最后四個(gè)構(gòu)造器之一,Thread(ThreadGroup group, Runnable target, String name, long stackSize),令人感興趣的是它能夠讓你指定想要的線程方法調(diào)用堆棧的大小。能夠指定大小將證明在使用遞歸方法(一種為何一個(gè)方法不斷重復(fù)調(diào)用自身的技術(shù))優(yōu)美地解決一些問(wèn)題的程序中是十分有幫助的。通過(guò)明確地設(shè)置堆棧大小,你有時(shí)能夠預(yù)防StackOverflowErrors。然而,太大將導(dǎo)致OutOfMemoryErrors。同樣,Sun將方法調(diào)用堆棧的大小看作平臺(tái)依賴。依賴平臺(tái),方法調(diào)用堆棧的大小可能改變。因此,在寫調(diào)用Thread(ThreadGroup group, Runnable target, String name, long stackSize)代碼前仔細(xì)考慮你的程序分枝。
          開(kāi)始你的運(yùn)載工具
          線程類似于運(yùn)載工具:它們將程序從開(kāi)始移動(dòng)到結(jié)束。Thread 和Thread子類對(duì)象不是線程。它們描述一個(gè)線程的屬性,比如名稱和包含線程執(zhí)行的代碼(經(jīng)由一個(gè)run()方法)。當(dāng)一個(gè)新線程執(zhí)行run()時(shí),另一個(gè)線程正調(diào)用Thread或其子類對(duì)象的start()方法。例如,要開(kāi)始第二個(gè)線程,應(yīng)用程序的開(kāi)始線程—它執(zhí)行main()—調(diào)用start()。作為響應(yīng),JVM和平臺(tái)一起工作的線程操作代碼確保線程正確地初始化并調(diào)用Thread或其子類對(duì)象的run()方法。
          一旦start()完成,多重線程便運(yùn)行。因?yàn)槲覀冓呄蛴谠谝环N線性的方式中思維,我們常發(fā)現(xiàn)當(dāng)兩個(gè)或更多線程正運(yùn)行時(shí)理解并發(fā)(同時(shí))行為是困難的。因此,你應(yīng)該看看顯示與時(shí)間對(duì)比一個(gè)線程正在哪里執(zhí)行(它的位置)的圖表。下圖就是這樣一個(gè)圖表。


          與時(shí)間對(duì)比一個(gè)開(kāi)始線程和一個(gè)新建線程執(zhí)行位置的行為

          圖表顯示了幾個(gè)重要的時(shí)間段:
          ·開(kāi)始線程的初始化
          ·線程開(kāi)始執(zhí)行main()瞬間
          ·線程開(kāi)始執(zhí)行start()的瞬間
          ·start()創(chuàng)建一個(gè)新線程并返回main()的瞬間
          ·新線程的初始化
          ·新線程開(kāi)始執(zhí)行run()的瞬間
          ·每個(gè)線程結(jié)束的不同瞬間
          注意新線程的初始化,它對(duì)run()的執(zhí)行,和它的結(jié)束都與開(kāi)始線程的執(zhí)行同時(shí)發(fā)生。
          警告
          一個(gè)線程調(diào)用start()后,在run()方法退出前并發(fā)調(diào)用那方法將導(dǎo)致start()擲出一個(gè)java.lang.IllegalThreadStateException對(duì)象。



          怎樣使用名稱
          在一個(gè)調(diào)試會(huì)話期間,使用用戶友好方式從另一個(gè)線程區(qū)別其中一個(gè)線程證明是有幫助的。要區(qū)分其中一個(gè)線程,Java給一個(gè)線程取一個(gè)名稱。Thread缺省的名稱是一個(gè)短線連字符和一個(gè)零開(kāi)始的數(shù)字符號(hào)。你可以接受Java的缺省線程名稱或選擇使用你自己的。為了能夠自定義名稱,Thread提供帶有name參數(shù)和一個(gè)setName(String name)方法的構(gòu)造器。Thread也提供一個(gè)getName()方法返回當(dāng)前名稱。表2顯示了怎樣通過(guò)Thread(String name)創(chuàng)建一個(gè)自定義名稱和通過(guò)在run()方法中調(diào)用getName()檢索當(dāng)前名稱:
          表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 ()
          {
          //編譯器創(chuàng)建等價(jià)于super()的字節(jié)代碼
          }
          MyThread (String name)
          {
          super (name); //將名稱傳遞給Thread超類
          }
          public void run ()
          {
          System.out.println ("My name is: " + getName ());
          }
          }
          你能夠在命令行向MyThread傳遞一個(gè)可選的name參數(shù)。例如,java NameThatThread X 建立X作為線程的名稱。如果你指定一個(gè)名稱失敗,你將看到下面的輸出:
          My name is: Thread-1
          如果你喜歡,你能夠在MyThread(String name)構(gòu)造器中將super(name)調(diào)用改變成setName(String name)調(diào)用——作為setName(name)后一種方法調(diào)用達(dá)到同樣建立線程名稱的目的——作為super(name)我作為練習(xí)保留給你們。
          注意
          Java主要將名稱指派給運(yùn)行main() 方法的線程,開(kāi)始線程。你特別要看看當(dāng)開(kāi)始線程擲出一個(gè)例外對(duì)象時(shí)在線程“main”的例外顯示的JVM的缺省例外處理打印消息。
          休眠或停止休眠
          在這一欄后面,我將向你介紹動(dòng)畫——在一個(gè)表面上重復(fù)畫圖形,這稍微不同于完成一個(gè)運(yùn)動(dòng)畫面。要完成動(dòng)畫,一個(gè)線程必須在它顯示兩個(gè)連續(xù)畫面時(shí)中止。調(diào)用Thread的靜態(tài)sleep(long millis)方法強(qiáng)迫一個(gè)線程中止millis毫秒。另一個(gè)線程可能中斷正在休眠的線程。如果這種事發(fā)生,正在休眠的線程將醒來(lái)并從sleep(long millis)方法擲出一個(gè)InterruptedException對(duì)象。結(jié)果,調(diào)用sleep(long millis)的代碼必須在一個(gè)try代碼塊中出現(xiàn)——或代碼方法必須在自己的throws子句中包括InterruptedException。
          為了示范sleep(long millis),我寫了一個(gè)CalcPI1應(yīng)用程序。這個(gè)應(yīng)用程序開(kāi)始了一個(gè)新線程便于用一個(gè)數(shù)學(xué)運(yùn)算法則計(jì)算數(shù)學(xué)常量pi的值。當(dāng)新線程計(jì)算時(shí),開(kāi)始線程通過(guò)調(diào)用sleep(long millis)中止10毫秒。在開(kāi)始線程醒后,它將打印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 < 100000; i += 2)
          {
          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");
          }
          }
          如果你運(yùn)行這個(gè)程序,你將看到輸出如下(但也可能不一樣):
          pi = -0.2146197014017295
          完成計(jì)算PI
          為什么輸出不正確呢?畢竟,pi的值應(yīng)近似等于3.14159。回答是:開(kāi)始線程醒得太快了。在新線程剛開(kāi)始計(jì)算pi時(shí),開(kāi)始線程就醒過(guò)來(lái)讀取pi的當(dāng)前值并打印其值。我們可以通過(guò)將10毫秒延遲增加為更長(zhǎng)的值來(lái)進(jìn)行補(bǔ)償。這一更長(zhǎng)的值(不幸的是它是依賴于平臺(tái)的)將給新線程一個(gè)機(jī)會(huì)在開(kāi)始線程醒過(guò)來(lái)之前完成計(jì)算。(后面,你將學(xué)到一種不依賴平臺(tái)的技術(shù),它將防止開(kāi)始線程醒來(lái)直到新線程完成。)
          注意
          線程同時(shí)提供一個(gè)sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因?yàn)槎鄶?shù)基于JVM的平臺(tái)都不支持納秒級(jí)的分解度,JVM 線程處理代碼將納秒數(shù)字四舍五入成毫秒數(shù)字的近似值。如果一個(gè)平臺(tái)不支持毫秒級(jí)的分解度,JVM 線程處理代碼將毫秒數(shù)字四舍五入成平臺(tái)支持的最小級(jí)分解度的近似倍數(shù)。
          它是死的還是活的?
          當(dāng)一個(gè)程序調(diào)用Thread的start()方法時(shí),在一個(gè)新線程調(diào)用run()之前有一個(gè)時(shí)間段(為了初始化)。run()返回后,在JVM清除線程之前有一段時(shí)間通過(guò)。JVM認(rèn)為線程立即激活優(yōu)先于線程調(diào)用run(),在線程執(zhí)行run()期間和run()返回后。在這時(shí)間間隔期間,Thread的isAlive()方法返回一個(gè)布爾真值。否則,方法返回一個(gè)假值。
          isAlive()在一個(gè)線程需要在第一個(gè)線程能夠檢查其它線程的結(jié)果之前等待另一個(gè)線程完成其run()方法的情形下證明是有幫助的。實(shí)質(zhì)上,那些需要等待的線程輸入一個(gè)while循環(huán)。當(dāng)isAlive()為其它線程返回真值時(shí),等待線程調(diào)用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費(fèi)更多的CPU循環(huán))。一旦isAlive()返回假值,等待線程便檢查其它線程的結(jié)果。
          你將在哪里使用這樣的技術(shù)呢?對(duì)于起動(dòng)器,一個(gè)CalcPI1的修改版本怎么樣,在打印pi的值前開(kāi)始線程在哪里等待新線程的完成?表4的CalcPI2源代碼示范了這一技術(shù):
          表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 < 100000; i += 2)
          {
          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的開(kāi)始線程在10毫秒時(shí)間間隔休眠,直到mt.isAlive ()返回假值。當(dāng)那些發(fā)生時(shí),開(kāi)始線程從它的while循環(huán)中退出并打印pi的內(nèi)容。如果你運(yùn)行這個(gè)程序,你將看到如下的輸出(但不一定一樣):
          完成計(jì)算PI
          pi = 3.1415726535897894
          這不,現(xiàn)在看上去更精確了?
          注意
          一個(gè)線程可能對(duì)它自己調(diào)用isAlive() 方法。然而,這毫無(wú)意義,因?yàn)閕sAlive()將一直返回真值。
          合力
          因?yàn)閣hile循環(huán)/isAlive()方法/sleep()方法技術(shù)證明是有用的,Sun將其打包進(jìn)三個(gè)方法組成的一個(gè)組合里:join(),join(long millis)和join(long millis, int nanos)。當(dāng)當(dāng)前線程想等待其它線程結(jié)束時(shí),經(jīng)由另一個(gè)線程的線程對(duì)象引用調(diào)用join()。相反,當(dāng)它想其中任意線程等待其它線程結(jié)束或等待直到millis毫秒和nanos納秒組合通過(guò)時(shí),當(dāng)前線程調(diào)用join(long millis)或join(long millis, int nanos)。(作為sleep()方法,JVM 線程處理代碼將對(duì)join(long millis)和join(long millis,int nanos)方法的參數(shù)值四舍五入。)表5的CalcPI3源代碼示范了一個(gè)對(duì)join()的調(diào)用:
          表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 < 100000; i += 2)
          {
          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的開(kāi)始線程等待與MyThread對(duì)象有關(guān)被mt引用的線程結(jié)束。接著開(kāi)始線程打印pi的值,其值與CalcPI2的輸出一樣。
          警告
          不要試圖將當(dāng)前線程與其自身連接,因?yàn)檫@樣當(dāng)前線程將要永遠(yuǎn)等待。


          怎樣使用名稱
          在一個(gè)調(diào)試會(huì)話期間,使用用戶友好方式從另一個(gè)線程區(qū)別其中一個(gè)線程證明是有幫助的。要區(qū)分其中一個(gè)線程,Java給一個(gè)線程取一個(gè)名稱。Thread缺省的名稱是一個(gè)短線連字符和一個(gè)零開(kāi)始的數(shù)字符號(hào)。你可以接受Java的缺省線程名稱或選擇使用你自己的。為了能夠自定義名稱,Thread提供帶有name參數(shù)和一個(gè)setName(String name)方法的構(gòu)造器。Thread也提供一個(gè)getName()方法返回當(dāng)前名稱。表2顯示了怎樣通過(guò)Thread(String name)創(chuàng)建一個(gè)自定義名稱和通過(guò)在run()方法中調(diào)用getName()檢索當(dāng)前名稱:
          表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 ()
          {
          //編譯器創(chuàng)建等價(jià)于super()的字節(jié)代碼
          }
          MyThread (String name)
          {
          super (name); //將名稱傳遞給Thread超類
          }
          public void run ()
          {
          System.out.println ("My name is: " + getName ());
          }
          }
          你能夠在命令行向MyThread傳遞一個(gè)可選的name參數(shù)。例如,java NameThatThread X 建立X作為線程的名稱。如果你指定一個(gè)名稱失敗,你將看到下面的輸出:
          My name is: Thread-1
          如果你喜歡,你能夠在MyThread(String name)構(gòu)造器中將super(name)調(diào)用改變成setName(String name)調(diào)用——作為setName(name)后一種方法調(diào)用達(dá)到同樣建立線程名稱的目的——作為super(name)我作為練習(xí)保留給你們。
          注意
          Java主要將名稱指派給運(yùn)行main() 方法的線程,開(kāi)始線程。你特別要看看當(dāng)開(kāi)始線程擲出一個(gè)例外對(duì)象時(shí)在線程“main”的例外顯示的JVM的缺省例外處理打印消息。
          休眠或停止休眠
          在這一欄后面,我將向你介紹動(dòng)畫——在一個(gè)表面上重復(fù)畫圖形,這稍微不同于完成一個(gè)運(yùn)動(dòng)畫面。要完成動(dòng)畫,一個(gè)線程必須在它顯示兩個(gè)連續(xù)畫面時(shí)中止。調(diào)用Thread的靜態(tài)sleep(long millis)方法強(qiáng)迫一個(gè)線程中止millis毫秒。另一個(gè)線程可能中斷正在休眠的線程。如果這種事發(fā)生,正在休眠的線程將醒來(lái)并從sleep(long millis)方法擲出一個(gè)InterruptedException對(duì)象。結(jié)果,調(diào)用sleep(long millis)的代碼必須在一個(gè)try代碼塊中出現(xiàn)——或代碼方法必須在自己的throws子句中包括InterruptedException。
          為了示范sleep(long millis),我寫了一個(gè)CalcPI1應(yīng)用程序。這個(gè)應(yīng)用程序開(kāi)始了一個(gè)新線程便于用一個(gè)數(shù)學(xué)運(yùn)算法則計(jì)算數(shù)學(xué)常量pi的值。當(dāng)新線程計(jì)算時(shí),開(kāi)始線程通過(guò)調(diào)用sleep(long millis)中止10毫秒。在開(kāi)始線程醒后,它將打印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 < 100000; i += 2)
          {
          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");
          }
          }
          如果你運(yùn)行這個(gè)程序,你將看到輸出如下(但也可能不一樣):
          pi = -0.2146197014017295
          完成計(jì)算PI
          為什么輸出不正確呢?畢竟,pi的值應(yīng)近似等于3.14159。回答是:開(kāi)始線程醒得太快了。在新線程剛開(kāi)始計(jì)算pi時(shí),開(kāi)始線程就醒過(guò)來(lái)讀取pi的當(dāng)前值并打印其值。我們可以通過(guò)將10毫秒延遲增加為更長(zhǎng)的值來(lái)進(jìn)行補(bǔ)償。這一更長(zhǎng)的值(不幸的是它是依賴于平臺(tái)的)將給新線程一個(gè)機(jī)會(huì)在開(kāi)始線程醒過(guò)來(lái)之前完成計(jì)算。(后面,你將學(xué)到一種不依賴平臺(tái)的技術(shù),它將防止開(kāi)始線程醒來(lái)直到新線程完成。)
          注意
          線程同時(shí)提供一個(gè)sleep(long millis, int nanos)方法,它將線程休眠millis 毫秒和nanos 納秒。因?yàn)槎鄶?shù)基于JVM的平臺(tái)都不支持納秒級(jí)的分解度,JVM 線程處理代碼將納秒數(shù)字四舍五入成毫秒數(shù)字的近似值。如果一個(gè)平臺(tái)不支持毫秒級(jí)的分解度,JVM 線程處理代碼將毫秒數(shù)字四舍五入成平臺(tái)支持的最小級(jí)分解度的近似倍數(shù)。
          它是死的還是活的?
          當(dāng)一個(gè)程序調(diào)用Thread的start()方法時(shí),在一個(gè)新線程調(diào)用run()之前有一個(gè)時(shí)間段(為了初始化)。run()返回后,在JVM清除線程之前有一段時(shí)間通過(guò)。JVM認(rèn)為線程立即激活優(yōu)先于線程調(diào)用run(),在線程執(zhí)行run()期間和run()返回后。在這時(shí)間間隔期間,Thread的isAlive()方法返回一個(gè)布爾真值。否則,方法返回一個(gè)假值。
          isAlive()在一個(gè)線程需要在第一個(gè)線程能夠檢查其它線程的結(jié)果之前等待另一個(gè)線程完成其run()方法的情形下證明是有幫助的。實(shí)質(zhì)上,那些需要等待的線程輸入一個(gè)while循環(huán)。當(dāng)isAlive()為其它線程返回真值時(shí),等待線程調(diào)用sleep(long millis) (或 sleep(long millis, int nanos))周期性地休眠 (避免浪費(fèi)更多的CPU循環(huán))。一旦isAlive()返回假值,等待線程便檢查其它線程的結(jié)果。
          你將在哪里使用這樣的技術(shù)呢?對(duì)于起動(dòng)器,一個(gè)CalcPI1的修改版本怎么樣,在打印pi的值前開(kāi)始線程在哪里等待新線程的完成?表4的CalcPI2源代碼示范了這一技術(shù):
          表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 < 100000; i += 2)
          {
          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的開(kāi)始線程在10毫秒時(shí)間間隔休眠,直到mt.isAlive ()返回假值。當(dāng)那些發(fā)生時(shí),開(kāi)始線程從它的while循環(huán)中退出并打印pi的內(nèi)容。如果你運(yùn)行這個(gè)程序,你將看到如下的輸出(但不一定一樣):
          完成計(jì)算PI
          pi = 3.1415726535897894
          這不,現(xiàn)在看上去更精確了?
          注意
          一個(gè)線程可能對(duì)它自己調(diào)用isAlive() 方法。然而,這毫無(wú)意義,因?yàn)閕sAlive()將一直返回真值。
          合力
          因?yàn)閣hile循環(huán)/isAlive()方法/sleep()方法技術(shù)證明是有用的,Sun將其打包進(jìn)三個(gè)方法組成的一個(gè)組合里:join(),join(long millis)和join(long millis, int nanos)。當(dāng)當(dāng)前線程想等待其它線程結(jié)束時(shí),經(jīng)由另一個(gè)線程的線程對(duì)象引用調(diào)用join()。相反,當(dāng)它想其中任意線程等待其它線程結(jié)束或等待直到millis毫秒和nanos納秒組合通過(guò)時(shí),當(dāng)前線程調(diào)用join(long millis)或join(long millis, int nanos)。(作為sleep()方法,JVM 線程處理代碼將對(duì)join(long millis)和join(long millis,int nanos)方法的參數(shù)值四舍五入。)表5的CalcPI3源代碼示范了一個(gè)對(duì)join()的調(diào)用:
          表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 < 100000; i += 2)
          {
          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的開(kāi)始線程等待與MyThread對(duì)象有關(guān)被mt引用的線程結(jié)束。接著開(kāi)始線程打印pi的值,其值與CalcPI2的輸出一樣。
          警告
          不要試圖將當(dāng)前線程與其自身連接,因?yàn)檫@樣當(dāng)前線程將要永遠(yuǎn)等待。


          查詢活躍線程
          在有些情形下,你可能想了解在你的程序中哪些線程是激活的。Thread支持一對(duì)方法幫助你完成這個(gè)任務(wù): activeCount()和 enumerate(Thread [] thdarray)。但那些方法只工作在當(dāng)前線程的線程組中。換句話說(shuō),那些方法只識(shí)別屬于當(dāng)前線程的同一線程組的活躍線程。 (我將在以后的系列文章中討論線程組——一種組織機(jī)制。)
          靜態(tài)activeCount()方法返回在當(dāng)前線程的線程組中正在活躍運(yùn)行的線程數(shù)量。一個(gè)程序利用這個(gè)方法的整數(shù)返回值設(shè)定一個(gè)Thread引用數(shù)組的大小。檢索那些引用,程序必須調(diào)用靜態(tài)enumerate(Thread [] thdarray)方法。這個(gè)方法的整數(shù)返回值確定Thread引用存貯在數(shù)組中的enumerate(Thread []thdarray)的總數(shù)。要看這些方法如何一起工作,請(qǐng)查看表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 < n; i++)
          System.out.println (threads [i].toString ());
          }
          }
          在運(yùn)行時(shí),這個(gè)程序會(huì)產(chǎn)生如下的輸出:
          Thread[main,5,main]
          輸出顯示一個(gè)線程,開(kāi)始線程正在運(yùn)行。左邊的main表示線程的名稱。5顯示線程的優(yōu)先權(quán),右邊的main表示線程的線程組。你也許很失望不能在輸出中看到任何系統(tǒng)線程,比如垃圾收集器線程。那種限制由Thread的enumerate(Thread [] thdarray) 方法產(chǎn)生,它僅詢問(wèn)當(dāng)前線程線程組的活躍線程。然而, ThreadGroup類包含多種enumerate()方法允許你捕獲對(duì)所有活躍線程的引用而不管線程組。在稍后的系列中,探討ThreadGroup時(shí)我將向你顯示如何列舉所有的引用。
          警告
          當(dāng)重申一個(gè)數(shù)組時(shí)不要依靠activeCount()的返回值。如果你這樣做了,你的程序?qū)⒚皵S出一個(gè)NullPointerException對(duì)象的風(fēng)險(xiǎn)。為什么呢?在調(diào)用activeCount()和enumerate(Thread [] thdarray)之間,一個(gè)或更多線程可能結(jié)束。結(jié)果, enumerate(Thread [] thdarray)能夠復(fù)制少數(shù)線程引用進(jìn)它的數(shù)組。因此,僅考慮將activeCount()的返回值作為數(shù)組可能大小的最大值。同樣,考慮將enumerate(Thread [] thdarray)的返回值作為在一個(gè)程序?qū)δ欠N方法調(diào)用時(shí)活躍線程的數(shù)目。
          反臭蟲
          如果你的程序出現(xiàn)故障并且你懷疑問(wèn)題出在線程,通過(guò)調(diào)用Thread的dumpStack()和toString()方法你能夠了解到線程的更多細(xì)節(jié)。靜態(tài)dumpStack()方法提供一個(gè)new Exception ("Stack trace").printStackTrace ()的封裝,打印一個(gè)追蹤當(dāng)前線程的堆棧。toString()依據(jù)下面格式返回一個(gè)描述線程的名稱、優(yōu)先權(quán)和線程組的字符串: Thread[thread-name,priority,thread-group]. (在稍后的系列中你將學(xué)到更多關(guān)于優(yōu)先權(quán)的知識(shí)。)
          技巧
          在一些地方,這篇文章提到了當(dāng)前線程的概念。如果你需要訪問(wèn)描述當(dāng)前線程的Thread對(duì)象,則調(diào)用Thread的靜態(tài)currentThread()方法。例:Thread current = Thread.currentThread ()。
          等級(jí)系統(tǒng)
          不是所有線程都被平等創(chuàng)建。它們被分成兩類:用戶和監(jiān)督。一個(gè)用戶線程執(zhí)行著對(duì)于程序用戶十分重要的工作,工作必須在程序結(jié)束前完成。相反,一個(gè)監(jiān)督線程執(zhí)行著后勤事務(wù)(比如垃圾收集)和其它可能不會(huì)對(duì)應(yīng)用程序的主要工作作出貢獻(xiàn)但對(duì)于應(yīng)用程序繼續(xù)它的主要工作卻非常必要的后臺(tái)任務(wù)。和用戶線程不一樣,監(jiān)督線程不需要在應(yīng)用程序結(jié)束前完成。當(dāng)一個(gè)應(yīng)用程序的開(kāi)始線程(它是一個(gè)用戶線程)結(jié)束時(shí),JVM檢查是否還有其它用戶線程正在運(yùn)行。如果有,JVM就會(huì)阻止應(yīng)用程序結(jié)束。否則,JVM就會(huì)結(jié)束應(yīng)用程序而不管監(jiān)督線程是否正在運(yùn)行。
          當(dāng)一個(gè)線程調(diào)用一個(gè)線程對(duì)象的start()方法時(shí),新的已經(jīng)開(kāi)始的線程就是一個(gè)用戶線程。那是缺省的。要建立一個(gè)線程作為監(jiān)督線程,程序必須在調(diào)用start()前調(diào)用Thread的一個(gè)帶布爾真值參數(shù)的setDaemon(boolean isDaemon)方法。稍后,你可以通過(guò)調(diào)用Thread的isDaemon()方法檢查一個(gè)線程是否是監(jiān)督線程。如果是監(jiān)督線程那個(gè)方法返回一個(gè)布爾真值。
          為了讓你試試用戶和監(jiān)督線程,我寫了一個(gè)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);
          }
          }
          編譯了代碼后,通過(guò)Java2 SDK的java命令運(yùn)行UserDaemonThreadDemo。如果你沒(méi)有使用命令行參數(shù)運(yùn)行程序,例如java UserDaemonThreadDemo, new MyThread ().start ()執(zhí)行。這段代碼片斷開(kāi)始一個(gè)在進(jìn)入一個(gè)無(wú)限循環(huán)前打印Daemon is false的用戶線程。(你必須按Ctrl-C或一個(gè)等價(jià)于結(jié)束一個(gè)無(wú)限循環(huán)的組合按鍵。)因?yàn)樾戮€程是一個(gè)用戶線程,應(yīng)用程序在開(kāi)始線程結(jié)束后仍保持運(yùn)行。然而,如果你指定了至少一個(gè)命令行參數(shù),例如java UserDaemonThreadDemo x,mt.setDaemon (true)執(zhí)行并且新線程將是一個(gè)監(jiān)督線程。結(jié)果,一旦開(kāi)始線程從100毫秒休眠中醒來(lái)并結(jié)束,新的監(jiān)督線程也將結(jié)束。
          警告
          如果線程開(kāi)始執(zhí)行后調(diào)用setDaemon(boolean isDaemon)方法,setDaemon(boolean isDaemon)方法將擲出一個(gè)IllegalThreadStateException對(duì)象。
          Runnable
          學(xué)習(xí)前面部份的例子后,你可能認(rèn)為引入多線程進(jìn)入一個(gè)類總是要求你去擴(kuò)展Thread并將你的子類重載Thread's run()方法。然而那并不總是一種選擇。Java對(duì)繼承的強(qiáng)制執(zhí)行禁止一個(gè)類擴(kuò)展兩個(gè)或更多個(gè)超類。結(jié)果,如果一個(gè)類擴(kuò)展了一個(gè)無(wú)線程類,那個(gè)類就不能擴(kuò)展Thread. 假使限制,怎樣才可能將多線程引入一個(gè)已經(jīng)擴(kuò)展了其它類的類?幸運(yùn)的是, Java的設(shè)計(jì)者已經(jīng)意識(shí)到不可能創(chuàng)建Thread子類的情形總會(huì)發(fā)生的。這導(dǎo)致產(chǎn)生java.lang.Runnable接口和帶Runnable參數(shù)的Thread構(gòu)造器,如Thread(Runnable target)。
          Runnable接口聲明了一個(gè)單獨(dú)方法署名:void run()。這個(gè)署名和Thread的run()方法署名一樣并作為線程的執(zhí)行入口服務(wù)。因?yàn)镽unnable是一個(gè)接口,任何類都能通過(guò)將一個(gè)implements子句包含進(jìn)類頭和提供一個(gè)適當(dāng)?shù)膔un()方法實(shí)現(xiàn)接口。在執(zhí)行時(shí)間,程序代碼能從那個(gè)類創(chuàng)建一個(gè)對(duì)象或runnable并將runnable的引用傳遞給一個(gè)適當(dāng)?shù)腡hread構(gòu)造器。構(gòu)造器和Thread對(duì)象一起存貯這個(gè)引用并確保一個(gè)新線程在調(diào)用Thread對(duì)象的start()方法后調(diào)用runnable的run()方法。示范如表8:
          表8.RunnableDemo.java
          // RunnableDemo.java
          class RunnableDemo
          {
          public static void main (String [] args)
          {
          Rectangle r = new Rectangle (5, 6);
          r.draw ();
          //用隨機(jī)選擇的寬度和高度畫不同的長(zhǎng)方形
          new Rectangle ();
          }
          }
          abstract class Shape
          {
          abstract void draw ();
          }
          class Rectangle extends Shape implements Runnable
          {
          private int w, h;
          Rectangle ()
          {
          //創(chuàng)建一個(gè)綁定這個(gè)runnable的新Thread對(duì)象并開(kāi)始一個(gè)將調(diào)用這個(gè)runnable的
          //run()方法的線程
          new Thread (this).start ();
          }
          Rectangle (int w, int h)
          {
          if (w < 2)
          throw new IllegalArgumentException ("w value " + w + " < 2");
          if (h < 2)
          throw new IllegalArgumentException ("h value " + h + " < 2");
          this.w = w;
          this.h = h;
          }
          void draw ()
          {
          for (int c = 0; c < w; c++)
          System.out.print ('*');
          System.out.print ('\n');
          for (int r = 0; r < h - 2; r++)
          {
          System.out.print ('*');
          for (int c = 0; c < w - 2; c++)
          System.out.print (' ');
          System.out.print ('*');
          System.out.print ('\n');
          }
          for (int c = 0; c < w; c++)
          System.out.print ('*');
          System.out.print ('\n');
          }
          public void run ()
          {
          for (int i = 0; i < 20; i++)
          {
          w = rnd (30);
          if (w < 2)
          w += 2;
          h = rnd (10);
          if (h < 2)
          h += 2;
          draw ();
          }
          }
          int rnd (int limit)
          {
          //在0<=x<界限范圍內(nèi)返回一個(gè)隨機(jī)數(shù)字x
          return (int) (Math.random () * limit);
          }
          }
          RunnableDemo由類RunnableDemo,Shape和Rectangle組成。類RunnableDemo通過(guò)創(chuàng)建一個(gè)Rectangle對(duì)象驅(qū)動(dòng)應(yīng)用程序—通過(guò)調(diào)用對(duì)象的draw()方法—和通過(guò)創(chuàng)建第二個(gè)什么都不做的Rectangle類。相反,Shape和Rectangle組成了一個(gè)基于shape層次的類。Shape是抽象的因?yàn)樗峁┮粋€(gè)抽象的draw()方法。各種shape類,比如Rectangle,擴(kuò)展Shape和描述它們?nèi)绾萎嬎鼈冏约旱闹剌ddraw()。以后,我可能決定引入一些另外的shape類,創(chuàng)建一個(gè)Shape數(shù)組,通過(guò)調(diào)用Shape的draw()方法要求每一個(gè)Shape元素畫它自己。
          RunnableDemo 作為一個(gè)不帶多線程的簡(jiǎn)單程序產(chǎn)生。后面我決定引入多線程到Rectangle,這樣我能夠用各種寬度和高度畫種種矩形。因?yàn)镽ectangle擴(kuò)展Shape (為了以后的多態(tài)性原因),我沒(méi)有其它選擇只有讓Rectangle實(shí)現(xiàn)Runnable。同樣,在Rectangle()構(gòu)造器內(nèi),我不得不將一個(gè)Rectangle runnable綁定到一個(gè)新的Thread對(duì)象并調(diào)用Thread的start()方法開(kāi)始一個(gè)新的線程調(diào)用Rectangle的run()方法畫矩形。
          因?yàn)榘ㄔ谶@篇文章中的RunnableDemo的新輸出太長(zhǎng)了,我建議你自己編譯并運(yùn)行程序。
          技巧
          當(dāng)你面對(duì)一個(gè)類不是能擴(kuò)展Thread就是能實(shí)現(xiàn)Runnable的情形時(shí),你將選擇哪種方法?如果這個(gè)類已經(jīng)擴(kuò)展了其它類,你必須實(shí)現(xiàn)Runnable。然而,如果這個(gè)類沒(méi)有擴(kuò)展其它類,考慮一下類的名稱。名稱將暗示這個(gè)類的對(duì)象不是積極的就是消極的。例如,名稱Ticker暗示它的對(duì)象是積極的。因此,Ticker類將擴(kuò)展Thread,并且Ticker對(duì)象將被作為專門的Thread對(duì)象。相反,Rectangle暗示消極對(duì)象—Rectangle對(duì)象對(duì)于它們自己什么也不做。因此,Rectangle類將實(shí)現(xiàn)Runnable,并且Rectangle 對(duì)象將使用Thread對(duì)象(為了測(cè)試或其它意圖)代替成為專門的Thread對(duì)象。
          回顧
          用戶期望程序達(dá)到優(yōu)異的性能。一種辦法是用線程完成那些任務(wù)。一個(gè)線程是一條程序代碼的獨(dú)立執(zhí)行通道。線程有益于基于GUI的程序,因?yàn)樗鼈冊(cè)试S那些程序當(dāng)執(zhí)行其它任務(wù)時(shí)仍對(duì)用戶保持響應(yīng)。另外,帶線程的程序比它們沒(méi)帶線程的副本程序完成的快。這對(duì)于運(yùn)行在多處理器機(jī)器上的情形尤其明顯,在這里每一個(gè)線程有它自己的處理器。Thread和Thread子類對(duì)象描述了線程并與那些實(shí)體相關(guān)。對(duì)于那些不能擴(kuò)展Thread的類,你必須創(chuàng)建一個(gè)runnable以利用多線程的優(yōu)勢(shì)。
          posted on 2005-08-04 14:59 nighthawk 閱讀(316) 評(píng)論(1)  編輯  收藏 所屬分類: 編程基礎(chǔ)

          Feedback

          # re: 介紹線程、線程類及Runnable。(轉(zhuǎn)) 2005-09-18 08:24 t
          ryt   回復(fù)  更多評(píng)論
            

          主站蜘蛛池模板: 乐业县| 江川县| 宁城县| 通化县| 东乡县| 屯门区| 上栗县| 台山市| 阳春市| 丽江市| 云浮市| 蒙阴县| 扬中市| 白山市| 沐川县| 肥乡县| 西林县| 涡阳县| 延边| 信丰县| 安泽县| 梧州市| 孟连| 天门市| 柯坪县| 新邵县| 成都市| 方正县| 惠来县| 荔浦县| 赤城县| 孙吴县| 洞头县| 始兴县| 四会市| 鲁山县| 南投县| 商南县| 华坪县| 建始县| 赞皇县|