?? chapter14.htm
字號:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>Thinking in Java | Chinese Version by Trans Bot</title>
<meta name="Microsoft Theme" content="inmotion 111, default"></head>
<body background="../_themes/inmotion/inmtextb.gif" tppabs="http://member.netease.com/%7etransbot/Thinking%20in%20Java/_themes/inmotion/inmtextb.gif" bgcolor="#FFFFCC" text="#000000" link="#800000" vlink="#996633" alink="#FF3399">
<p>第14章 多線程<br>
<br>
利用對象,可將一個程序分割成相互獨立的區域。我們通常也需要將一個程序轉換成多個獨立運行的子任務。<br>
象這樣的每個子任務都叫作一個“線程”(Thread)。編寫程序時,可將每個線程都想象成獨立運行,而且都有自己的專用CPU。一些基礎機制實際會為我們自動分割CPU的時間。我們通常不必關心這些細節問題,所以多線程的代碼編寫是相當簡便的。<br>
這時理解一些定義對以后的學習狠有幫助。“進程”是指一種“自包容”的運行程序,有自己的地址空間。“多任務”操作系統能同時運行多個進程(程序)——但實際是由于CPU分時機制的作用,使每個進程都能循環獲得自己的CPU時間片。但由于輪換速度非常快,使得所有程序好象是在“同時”運行一樣。“線程”是進程內部單一的一個順序控制流。因此,一個進程可能容納了多個同時執行的線程。<br>
多線程的應用范圍很廣。但在一般情況下,程序的一些部分同特定的事件或資源聯系在一起,同時又不想為它而暫停程序其他部分的執行。這樣一來,就可考慮創建一個線程,令其與那個事件或資源關聯到一起,并讓它獨立于主程序運行。一個很好的例子便是“Quit”或“退出”按鈕——我們并不希望在程序的每一部分代碼中都輪詢這個按鈕,同時又希望該按鈕能及時地作出響應(使程序看起來似乎經常都在輪詢它)。事實上,多線程最主要的一個用途就是構建一個“反應靈敏”的用戶界面。<br>
<br>
14.1 反應靈敏的用戶界面<br>
作為我們的起點,請思考一個需要執行某些CPU密集型計算的程序。由于CPU“全心全意”為那些計算服務,所以對用戶的輸入十分遲鈍,幾乎沒有什么反應。在這里,我們用一個合成的applet/application(程序片/應用程序)來簡單顯示出一個計數器的結果:<br>
<br>
752-753頁程序<br>
<br>
在這個程序中,AWT和程序片代碼都應是大家熟悉的,第13章對此已有很詳細的交待。go()方法正是程序全心全意服務的對待:將當前的count(計數)值置入TextField(文本字段)t,然后使count增值。<br>
go()內的部分無限循環是調用sleep()。sleep()必須同一個Thread(線程)對象關聯到一起,而且似乎每個應用程序都有部分線程同它關聯(事實上,Java本身就是建立在線程基礎上的,肯定有一些線程會伴隨我們寫的應用一起運行)。所以無論我們是否明確使用了線程,都可利用Thread.currentThread()產生由程序使用的當前線程,然后為那個線程調用sleep()。注意,Thread.currentThread()是Thread類的一個靜態方法。<br>
注意sleep()可能“擲”出一個InterruptException(中斷違例)——盡管產生這樣的違例被認為是中止線程的一種“惡意”手段,而且應該盡可能地杜絕這一做法。再次提醒大家,違例是為異常情況而產生的,而不是為了正常的控制流。在這里包含了對一個“睡眠”線程的中斷,以支持未來的一種語言特性。<br>
一旦按下start按鈕,就會調用go()。研究一下go(),你可能會很自然地(就象我一樣)認為它該支持多線程,因為它會進入“睡眠”狀態。也就是說,盡管方法本身“睡著”了,CPU仍然應該忙于監視其他按鈕“按下”事件。但有一個問題,那就是go()是永遠不會返回的,因為它被設計成一個無限循環。這意味著actionPerformed()根本不會返回。由于在第一個按鍵以后便陷入actionPerformed()中,所以程序不能再對其他任何事件進行控制(如果想出來,必須以某種方式“殺死”進程——最簡便的方式就是在控制臺窗口按Ctrl+C鍵)。<br>
這里最基本的問題是go()需要繼續執行自己的操作,而與此同時,它也需要返回,以便actionPerformed()能夠完成,而且用戶界面也能繼續響應用戶的操作。但對象go()這樣的傳統方法來說,它卻不能在繼續的同時將控制權返回給程序的其他部分。這聽起來似乎是一件不可能做到的事情,就象CPU必須同時位于兩個地方一樣,但線程可以解決一切。“線程模型”(以及Java中的編程支持)是一種程序編寫規范,可在單獨一個程序里實現幾個操作的同時進行。根據這一機制,CPU可為每個線程都分配自己的一部分時間。每個線程都“感覺”自己好象擁有整個CPU,但CPU的計算時間實際卻是在所有線程間分攤的。<br>
線程機制多少降低了一些計算效率,但無論程序的設計,資源的均衡,還是用戶操作的方便性,都從中獲得了巨大的利益。綜合考慮,這一機制是非常有價值的。當然,如果本來就安裝了多塊CPU,那么操作系統能夠自行決定為不同的CPU分配哪些線程,程序的總體運行速度也會變得更快(所有這些都要求操作系統以及應用程序的支持)。多線程和多任務是充分發揮多處理機系統能力的一種最有效的方式。<br>
<br>
14.1.1 從線程繼承<br>
為創建一個線程,最簡單的方法就是從Thread類繼承。這個類包含了創建和運行線程所需的一切東西。Thread最重要的方法是run()。但為了使用run(),必須對其進行過載或者覆蓋,使其能充分按自己的吩咐行事。因此,run()屬于那些會與程序中的其他線程“并發”或“同時”執行的代碼。<br>
下面這個例子可創建任意數量的線程,并通過為每個線程分配一個獨一無二的編號(由一個靜態變量產生),從而對不同的線程進行跟蹤。Thread的run()方法在這里得到了覆蓋,每通過一次循環,計數就減1——計數為0時則完成循環(此時一旦返回run(),線程就中止運行)。<br>
<br>
755頁程序<br>
<br>
run()方法幾乎肯定含有某種形式的循環——它們會一直持續到線程不再需要為止。因此,我們必須規定特定的條件,以便中斷并退出這個循環(或者在上述的例子中,簡單地從run()返回即可)。run()通常采用一種無限循環的形式。也就是說,通過阻止外部發出對線程的stop()或者destroy()調用,它會永遠運行下去(直到程序完成)。<br>
在main()中,可看到創建并運行了大量線程。Thread包含了一個特殊的方法,叫作start(),它的作用是對線程進行特殊的初始化,然后調用run()。所以整個步驟包括:調用構建器來構建對象,然后用start()配置線程,再調用run()。如果不調用start()——如果適當的話,可在構建器那樣做——線程便永遠不會啟動。<br>
下面是該程序某一次運行的輸出(注意每次運行都會不同):<br>
<br>
756頁程序<br>
<br>
可注意到這個例子中到處都調用了sleep(),然而輸出結果指出每個線程都獲得了屬于自己的那一部分CPU執行時間。從中可以看出,盡管sleep()依賴一個線程的存在來執行,但卻與允許或禁止線程無關。它只不過是另一個不同的方法而已。<br>
亦可看出線程并不是按它們創建時的順序運行的。事實上,CPU處理一個現有線程集的順序是不確定的——除非我們親自介入,并用Thread的setPriority()方法調整它們的優先級。<br>
main()創建Thread對象時,它并未捕獲任何一個對象的句柄。普通對象對于垃圾收集來說是一種“公平競賽”,但線程卻并非如此。每個線程都會“注冊”自己,所以某處實際存在著對它的一個引用。這樣一來,垃圾收集器便只好對它“瞠目以對”了。<br>
<br>
14.1.2 針對用戶界面的多線程<br>
現在,我們也許能用一個線程解決在Counter1.java中出現的問題。采用的一個技巧便是在一個線程的run()方法中放置“子任務”——亦即位于go()內的循環。一旦用戶按下Start按鈕,線程就會啟動,但馬上結束線程的創建。這樣一來,盡管線程仍在運行,但程序的主要工作卻能得以繼續(等候并響應用戶界面的事件)。下面是具體的代碼:<br>
<br>
757-759頁程序<br>
<br>
現在,Counter2變成了一個相當直接的程序,它的唯一任務就是設置并管理用戶界面。但假若用戶現在按下Start按鈕,卻不會真正調用一個方法。此時不是創建類的一個線程,而是創建SeparateSubTask,然后繼續Counter2事件循環。注意此時會保存SeparateSubTask的句柄,以便我們按下onOff按鈕的時候,能正常地切換位于SeparateSubTask內部的runFlag(運行標志)。隨后那個線程便可啟動(當它看到標志的時候),然后將自己中止(亦可將SeparateSubTask設為一個內部類來達到這一目的)。<br>
SeparateSubTask類是對Thread的一個簡單擴展,它帶有一個構建器(其中保存了Counter2句柄,然后通過調用start()來運行線程)以及一個run()——本質上包含了Counter1.java的go()內的代碼。由于SeparateSubTask知道自己容納了指向一個Counter2的句柄,所以能夠在需要的時候介入,并訪問Counter2的TestField(文本字段)。<br>
按下onOff按鈕,幾乎立即能得到正確的響應。當然,這個響應其實并不是“立即”發生的,它畢竟和那種由“中斷”驅動的系統不同。只有線程擁有CPU的執行時間,并注意到標記已發生改變,計數器才會停止。<br>
<br>
1. 用內部類改善代碼<br>
下面說說題外話,請大家注意一下SeparateSubTask和Counter2類之間發生的結合行為。SeparateSubTask同Counter2“親密”地結合到了一起——它必須持有指向自己“父”Counter2對象的一個句柄,以便自己能回調和操縱它。但兩個類并不是真的合并為單獨一個類(盡管在下一節中,我們會講到Java確實提供了合并它們的方法),因為它們各自做的是不同的事情,而且是在不同的時間創建的。但不管怎樣,它們依然緊密地結合到一起(更準確地說,應該叫“聯合”),所以使程序代碼多少顯得有些笨拙。在這種情況下,一個內部類可以顯著改善代碼的“可讀性”和執行效率:<br>
<br>
759-761頁程序<br>
<br>
這個SeparateSubTask名字不會與前例中的SeparateSubTask沖突——即使它們都在相同的目錄里——因為它已作為一個內部類隱藏起來。大家亦可看到內部類被設為private(私有)屬性,這意味著它的字段和方法都可獲得默認的訪問權限(run()除外,它必須設為public,因為它在基礎類中是公開的)。除Counter2i之外,其他任何方面都不可訪問private內部類。而且由于兩個類緊密結合在一起,所以很容易放寬它們之間的訪問限制。在SeparateSubTask中,我們可看到invertFlag()方法已被刪去,因為Counter2i現在可以直接訪問runFlag。<br>
此外,注意SeparateSubTask的構建器已得到了簡化——它現在唯一的用外就是啟動線程。Counter2i對象的句柄仍象以前那樣得以捕獲,但不再是通過人工傳遞和引用外部對象來達到這一目的,此時的內部類機制可以自動照料它。在run()中,可看到對t的訪問是直接進行的,似乎它是SeparateSubTask的一個字段。父類中的t字段現在可以變成private,因為SeparateSubTask能在未獲任何特殊許可的前提下自由地訪問它——而且無論如何都該盡可能地把字段變成“私有”屬性,以防來自類外的某種力量不慎地改變它們。<br>
無論在什么時候,只要注意到類相互之間結合得比較緊密,就可考慮利用內部類來改善代碼的編寫與維護。<br>
<br>
14.1.3 用主類合并線程<br>
在上面的例子中,我們看到線程類(Thread)與程序的主類(Main)是分隔開的。這樣做非常合理,而且易于理解。然而,還有另一種方式也是經常要用到的。盡管它不十分明確,但一般都要更簡潔一些(這也解釋了它為什么十分流行)。通過將主程序類變成一個線程,這種形式可將主程序類與線程類合并到一起。由于對一個GUI程序來說,主程序類必須從Frame或Applet繼承,所以必須用一個接口加入額外的功能。這個接口叫作Runnable,其中包含了與Thread一致的基本方法。事實上,Thread也實現了Runnable,它只指出有一個run()方法。<br>
對合并后的程序/線程來說,它的用法不是十分明確。當我們啟動程序時,會創建一個Runnable(可運行的)對象,但不會自行啟動線程。線程的啟動必須明確進行。下面這個程序向我們演示了這一點,它再現了Counter2的功能:<br>
<br>
762-763頁程序1<br>
<br>
現在run()位于類內,但它在init()結束以后仍處在“睡眠”狀態。若按下啟動按鈕,線程便會用多少有些曖昧的表達方式創建(若線程尚不存在):<br>
new Thread(Counter3.this);<br>
若某樣東西有一個Runnable接口,實際只是意味著它有一個run()方法,但不存在與之相關的任何特殊東西——它不具有任何天生的線程處理能力,這與那些從Thread繼承的類是不同的。所以為了從一個Runnable對象產生線程,必須單獨創建一個線程,并為其傳遞Runnable對象;可為其使用一個特殊的構建器,并令其采用一個Runnable作為自己的參數使用。隨后便可為那個線程調用start(),如下所示:<br>
selfThread.start();<br>
它的作用是執行常規初始化操作,然后調用run()。<br>
Runnable接口最大的一個優點是所有東西都從屬于相同的類。若需訪問什么東西,只需簡單地訪問它即可,不需要涉及一個獨立的對象。但為這種便利也是要付出代價的——只可為那個特定的對象運行單獨一個線程(盡管可創建那種類型的多個對象,或者在不同的類里創建其他對象)。<br>
注意Runnable接口本身并不是造成這一限制的罪魁禍首。它是由于Runnable與我們的主類合并造成的,因為每個應用只能主類的一個對象。<br>
<br>
14.1.4 制作多個線程<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -