?? 0120.htm
字號(hào):
selfThread.start();<br>
它的作用是執(zhí)行常規(guī)初始化操作,然后調(diào)用run()。<br>
Runnable接口最大的一個(gè)優(yōu)點(diǎn)是所有東西都從屬于相同的類。若需訪問(wèn)什么東西,只需簡(jiǎn)單地訪問(wèn)它即可,不需要涉及一個(gè)獨(dú)立的對(duì)象。但為這種便利也是要付出代價(jià)的——只可為那個(gè)特定的對(duì)象運(yùn)行單獨(dú)一個(gè)線程(盡管可創(chuàng)建那種類型的多個(gè)對(duì)象,或者在不同的類里創(chuàng)建其他對(duì)象)。<br>
注意Runnable接口本身并不是造成這一限制的罪魁禍?zhǔn)住K怯捎赗unnable與我們的主類合并造成的,因?yàn)槊總€(gè)應(yīng)用只能主類的一個(gè)對(duì)象。<br>
<br>
14.1.4 制作多個(gè)線程<br>
現(xiàn)在考慮一下創(chuàng)建多個(gè)不同的線程的問(wèn)題。我們不可用前面的例子來(lái)做到這一點(diǎn),所以必須倒退回去,利用從Thread繼承的多個(gè)獨(dú)立類來(lái)封裝run()。但這是一種更常規(guī)的方案,而且更易理解,所以盡管前例揭示了我們經(jīng)常都能看到的編碼樣式,但并不推薦在大多數(shù)情況下都那樣做,因?yàn)樗皇巧晕?fù)雜一些,而且靈活性稍低一些。<br>
下面這個(gè)例子用計(jì)數(shù)器和切換按鈕再現(xiàn)了前面的編碼樣式。但這一次,一個(gè)特定計(jì)數(shù)器的所有信息(按鈕和文本字段)都位于它自己的、從Thread繼承的對(duì)象內(nèi)。Ticker中的所有字段都具有private(私有)屬性,這意味著Ticker的具體實(shí)現(xiàn)方案可根據(jù)實(shí)際情況任意修改,其中包括修改用于獲取和顯示信息的數(shù)據(jù)組件的數(shù)量及類型。創(chuàng)建好一個(gè)Ticker對(duì)象以后,構(gòu)建器便請(qǐng)求一個(gè)AWT容器(Container)的句柄——Ticker用自己的可視組件填充那個(gè)容器。采用這種方式,以后一旦改變了可視組件,使用Ticker的代碼便不需要另行修改一道。<br>
<br>
764-766頁(yè)程序<br>
<br>
Ticker不僅包括了自己的線程處理機(jī)制,也提供了控制與顯示線程的工具。可按自己的意愿創(chuàng)建任意數(shù)量的線程,毋需明確地創(chuàng)建窗口化組件。<br>
在Counter4中,有一個(gè)名為s的Ticker對(duì)象的數(shù)組。為獲得最大的靈活性,這個(gè)數(shù)組的長(zhǎng)度是用程序片參數(shù)接觸Web頁(yè)而初始化的。下面是網(wǎng)頁(yè)中長(zhǎng)度參數(shù)大致的樣子,它們嵌于對(duì)程序片(applet)的描述內(nèi)容中:<br>
<applet code=Counter4 width=600 height=600><br>
<param name=size value="20"><br>
</applet><br>
其中,param,name和value是所有Web頁(yè)都適用的關(guān)鍵字。name是指程序中對(duì)參數(shù)的一種引用稱謂,value可以是任何字串(并不僅僅是解析成一個(gè)數(shù)字的東西)。<br>
我們注意到對(duì)數(shù)組s長(zhǎng)度的判斷是在init()內(nèi)部完成的,它沒(méi)有作為s的內(nèi)嵌定義的一部分提供。換言之,不可將下述代碼作為類定義的一部分使用(應(yīng)該位于任何方法的外部):<br>
inst size = Integer.parseInt(getParameter("Size"));<br>
Ticker[] s = new Ticker[size]<br>
可把它編譯出來(lái),但會(huì)在運(yùn)行期得到一個(gè)空指針違例。但若將getParameter()初始化移入init(),則可正常工作。程序片框架會(huì)進(jìn)行必要的啟動(dòng)工作,以便在進(jìn)入init()前收集好一些參數(shù)。<br>
此外,上述代碼被同時(shí)設(shè)置成一個(gè)程序片和一個(gè)應(yīng)用(程序)。在它是應(yīng)用程序的情況下,size參數(shù)可從命令行里提取出來(lái)(否則就提供一個(gè)默認(rèn)的值)。<br>
數(shù)組的長(zhǎng)度建好以后,就可以創(chuàng)建新的Ticker對(duì)象;作為T(mén)icker構(gòu)建器的一部分,用于每個(gè)Ticker的按鈕和文本字段就會(huì)加入程序片。<br>
按下Start按鈕后,會(huì)在整個(gè)Ticker數(shù)組里遍歷,并為每個(gè)Ticker調(diào)用start()。記住,start()會(huì)進(jìn)行必要的線程初始化工作,然后為那個(gè)線程調(diào)用run()。<br>
ToggleL監(jiān)視器只是簡(jiǎn)單地切換Ticker中的標(biāo)記,一旦對(duì)應(yīng)線程以后需要修改這個(gè)標(biāo)記,它會(huì)作出相應(yīng)的反應(yīng)。<br>
這個(gè)例子的一個(gè)好處是它使我們能夠方便地創(chuàng)建由單獨(dú)子任務(wù)構(gòu)成的大型集合,并以監(jiān)視它們的行為。在這種情況下,我們會(huì)發(fā)現(xiàn)隨著子任務(wù)數(shù)量的增多,機(jī)器顯示出來(lái)的數(shù)字可能會(huì)出現(xiàn)更大的分歧,這是由于為線程提供服務(wù)的方式造成的。<br>
亦可試著體驗(yàn)一下sleep(100)在Ticker.run()中的重要作用。若刪除sleep(),那么在按下一個(gè)切換按鈕前,情況仍然會(huì)進(jìn)展良好。按下按鈕以后,那個(gè)特定的線程就會(huì)出現(xiàn)一個(gè)失敗的runFlag,而且run()會(huì)深深地陷入一個(gè)無(wú)限循環(huán)——很難在多任務(wù)處理期間中止退出。因此,程序?qū)τ脩舨僮鞯姆磻?yīng)靈敏度會(huì)大幅度降低。<br>
<br>
14.1.5 Daemon線程<br>
“Daemon”線程的作用是在程序的運(yùn)行期間于后臺(tái)提供一種“常規(guī)”服務(wù),但它并不屬于程序的一個(gè)基本部分。因此,一旦所有非Daemon線程完成,程序也會(huì)中止運(yùn)行。相反,假若有任何非Daemon線程仍在運(yùn)行(比如還有一個(gè)正在運(yùn)行main()的線程),則程序的運(yùn)行不會(huì)中止。<br>
通過(guò)調(diào)用isDaemon(),可調(diào)查一個(gè)線程是不是一個(gè)Daemon,而且能用setDaemon()打開(kāi)或者關(guān)閉一個(gè)線程的Daemon狀態(tài)。如果是一個(gè)Daemon線程,那么它創(chuàng)建的任何線程也會(huì)自動(dòng)具備Daemon屬性。<br>
下面這個(gè)例子演示了Daemon線程的用法:<br>
<br>
768-769頁(yè)程序<br>
<br>
Daemon線程可將自己的Daemon標(biāo)記設(shè)置成“真”,然后產(chǎn)生一系列其他線程,而且認(rèn)為它們也具有Daemon屬性。隨后,它進(jìn)入一個(gè)無(wú)限循環(huán),在其中調(diào)用yield(),放棄對(duì)其他進(jìn)程的控制。在這個(gè)程序早期的一個(gè)版本中,無(wú)限循環(huán)會(huì)使int計(jì)數(shù)器增值,但會(huì)使整個(gè)程序都好象陷入停頓狀態(tài)。換用yield()后,卻可使程序充滿“活力”,不會(huì)使人產(chǎn)生停滯或反應(yīng)遲鈍的感覺(jué)。<br>
一旦main()完成自己的工作,便沒(méi)有什么能阻止程序中斷運(yùn)行,因?yàn)檫@里運(yùn)行的只有Daemon線程。所以能看到啟動(dòng)所有Daemon線程后顯示出來(lái)的結(jié)果,System.in也進(jìn)行了相應(yīng)的設(shè)置,使程序中斷前能等待一個(gè)回車。如果不進(jìn)行這樣的設(shè)置,就只能看到創(chuàng)建Daemon線程的一部分結(jié)果(試試將readLine()代碼換成不同長(zhǎng)度的sleep()調(diào)用,看看會(huì)有什么表現(xiàn))。<br>
<br>
14.2 共享有限的資源<br>
可將單線程程序想象成一種孤立的實(shí)體,它能遍歷我們的問(wèn)題空間,而且一次只能做一件事情。由于只有一個(gè)實(shí)體,所以永遠(yuǎn)不必?fù)?dān)心會(huì)有兩個(gè)實(shí)體同時(shí)試圖使用相同的資源,就象兩個(gè)人同時(shí)都想停到一個(gè)車位,同時(shí)都想通過(guò)一扇門(mén),甚至同時(shí)發(fā)話。<br>
進(jìn)入多線程環(huán)境后,它們則再也不是孤立的。可能會(huì)有兩個(gè)甚至更多的線程試圖同時(shí)同一個(gè)有限的資源。必須對(duì)這種潛在資源沖突進(jìn)行預(yù)防,否則就可能發(fā)生兩個(gè)線程同時(shí)訪問(wèn)一個(gè)銀行帳號(hào),打印到同一臺(tái)計(jì)算機(jī),以及對(duì)同一個(gè)值進(jìn)行調(diào)整等等。<br>
<br>
14.2.1 資源訪問(wèn)的錯(cuò)誤方法<br>
現(xiàn)在考慮換成另一種方式來(lái)使用本章頻繁見(jiàn)到的計(jì)數(shù)器。在下面的例子中,每個(gè)線程都包含了兩個(gè)計(jì)數(shù)器,它們?cè)趓un()里增值以及顯示。除此以外,我們使用了Watcher類的另一個(gè)線程。它的作用是監(jiān)視計(jì)數(shù)器,檢查它們是否保持相等。這表面是一項(xiàng)無(wú)意義的行動(dòng),因?yàn)槿绻榭创a,就會(huì)發(fā)現(xiàn)計(jì)數(shù)器肯定是相同的。但實(shí)際情況卻不一定如此。下面是程序的第一個(gè)版本:<br>
<br>
770-773頁(yè)程序<br>
<br>
和往常一樣,每個(gè)計(jì)數(shù)器都包含了自己的顯示組件:兩個(gè)文本字段以及一個(gè)標(biāo)簽。根據(jù)它們的初始值,可知道計(jì)數(shù)是相同的。這些組件在TwoCounter構(gòu)建器加入Container。由于這個(gè)線程是通過(guò)用戶的一個(gè)“按下按鈕”操作啟動(dòng)的,所以start()可能被多次調(diào)用。但對(duì)一個(gè)線程來(lái)說(shuō),對(duì)Thread.start()的多次調(diào)用是非法的(會(huì)產(chǎn)生違例)。在started標(biāo)記和過(guò)載的start()方法中,大家可看到針對(duì)這一情況采取的防范措施。<br>
在run()中,count1和count2的增值與顯示方式表面上似乎能保持它們完全一致。隨后會(huì)調(diào)用sleep();若沒(méi)有這個(gè)調(diào)用,程序便會(huì)出錯(cuò),因?yàn)槟菚?huì)造成CPU難于交換任務(wù)。<br>
synchTest()方法采取的似乎是沒(méi)有意義的行動(dòng),它檢查count1是否等于count2;如果不等,就把標(biāo)簽設(shè)為“Unsynched”(不同步)。但是首先,它調(diào)用的是類Sharing1的一個(gè)靜態(tài)成員,以便增值和顯示一個(gè)訪問(wèn)計(jì)數(shù)器,指出這種檢查已成功進(jìn)行了多少次(這樣做的理由會(huì)在本例的其他版本中變得非常明顯)。<br>
Watcher類是一個(gè)線程,它的作用是為處于活動(dòng)狀態(tài)的所有TwoCounter對(duì)象都調(diào)用synchTest()。其間,它會(huì)對(duì)Sharing1對(duì)象中容納的數(shù)組進(jìn)行遍歷。可將Watcher想象成它掠過(guò)TwoCounter對(duì)象的肩膀不斷地“偷看”。<br>
Sharing1包含了TwoCounter對(duì)象的一個(gè)數(shù)組,它通過(guò)init()進(jìn)行初始化,并在我們按下“start”按鈕后作為線程啟動(dòng)。以后若按下“Observe”(觀察)按鈕,就會(huì)創(chuàng)建一個(gè)或者多個(gè)觀察器,并對(duì)毫不設(shè)防的TwoCounter進(jìn)行調(diào)查。<br>
注意為了讓它作為一個(gè)程序片在瀏覽器中運(yùn)行,Web頁(yè)需要包含下面這幾行:<br>
<br>
774頁(yè)上程序<br>
<br>
可自行改變寬度、高度以及參數(shù),根據(jù)自己的意愿進(jìn)行試驗(yàn)。若改變了size和observers,程序的行為也會(huì)發(fā)生變化。我們也注意到,通過(guò)從命令行接受參數(shù)(或者使用默認(rèn)值),它被設(shè)計(jì)成作為一個(gè)獨(dú)立的應(yīng)用程序運(yùn)行。<br>
下面才是最讓人“不可思議”的。在TwoCounter.run()中,無(wú)限循環(huán)只是不斷地重復(fù)相鄰的行:<br>
t1.setText(Integer.toString(count1++));<br>
t2.setText(Integer.toString(count2++));<br>
(和“睡眠”一樣,不過(guò)在這里并不重要)。但在程序運(yùn)行的時(shí)候,你會(huì)發(fā)現(xiàn)count1和count2被“觀察”(用Watcher觀察)的次數(shù)是不相等的!這是由線程的本質(zhì)造成的——它們可在任何時(shí)候掛起(暫停)。所以在上述兩行的執(zhí)行時(shí)刻之間,有時(shí)會(huì)出現(xiàn)執(zhí)行暫停現(xiàn)象。同時(shí),Watcher線程也正好跟隨著進(jìn)來(lái),并正好在這個(gè)時(shí)候進(jìn)行比較,造成計(jì)數(shù)器出現(xiàn)不相等的情況。<br>
本例揭示了使用線程時(shí)一個(gè)非常基本的問(wèn)題。我們跟無(wú)從知道一個(gè)線程什么時(shí)候運(yùn)行。想象自己坐在一張桌子前面,桌上放有一把叉子,準(zhǔn)備叉起自己的最后一塊食物。當(dāng)叉子要碰到食物時(shí),食物卻突然消失了(因?yàn)檫@個(gè)線程已被掛起,同時(shí)另一個(gè)線程進(jìn)來(lái)“偷”走了食物)。這便是我們要解決的問(wèn)題。<br>
有的時(shí)候,我們并不介意一個(gè)資源在嘗試使用它的時(shí)候是否正被訪問(wèn)(食物在另一些盤(pán)子里)。但為了讓多線程機(jī)制能夠正常運(yùn)轉(zhuǎn),需要采取一些措施來(lái)防止兩個(gè)線程訪問(wèn)相同的資源——至少在關(guān)鍵的時(shí)期。<br>
為防止出現(xiàn)這樣的沖突,只需在線程使用一個(gè)資源時(shí)為其加鎖即可。訪問(wèn)資源的第一個(gè)線程會(huì)其加上鎖以后,其他線程便不能再使用那個(gè)資源,除非被解鎖。如果車子的前座是有限的資源,高喊“這是我的!”的孩子會(huì)主張把它鎖起來(lái)。<br>
<br>
14.2.2 Java如何共享資源<br>
對(duì)一種特殊的資源——對(duì)象中的內(nèi)存——Java提供了內(nèi)建的機(jī)制來(lái)防止它們的沖突。由于我們通常將數(shù)據(jù)元素設(shè)為從屬于private(私有)類,然后只通過(guò)方法訪問(wèn)那些內(nèi)存,所以只需將一個(gè)特定的方法設(shè)為synchronized(同步的),便可有效地防止沖突。在任何時(shí)刻,只可有一個(gè)線程調(diào)用特定對(duì)象的一個(gè)synchronized方法(盡管那個(gè)線程可以調(diào)用多個(gè)對(duì)象的同步方法)。下面列出簡(jiǎn)單的synchronized方法:<br>
synchronized void f() { /* ... */ }<br>
synchronized void g() { /* ... */ }<br>
每個(gè)對(duì)象都包含了一把鎖(也叫作“監(jiān)視器”),它自動(dòng)成為對(duì)象的一部分(不必為此寫(xiě)任何特殊的代碼)。調(diào)用任何synchronized方法時(shí),對(duì)象就會(huì)被鎖定,不可再調(diào)用那個(gè)對(duì)象的其他任何synchronized方法,除非第一個(gè)方法完成了自己的工作,并解除鎖定。在上面的例子中,如果為一個(gè)對(duì)象調(diào)用f(),便不能再為同樣的對(duì)象調(diào)用g(),除非f()完成并解除鎖定。因此,一個(gè)特定對(duì)象的所有synchronized方法都共享著一把鎖,而且這把鎖能防止多個(gè)方法對(duì)通用內(nèi)存同時(shí)進(jìn)行寫(xiě)操作(比如同時(shí)有多個(gè)線程)。<br>
每個(gè)類也有自己的一把鎖(作為類的Class對(duì)象的一部分),所以synchronized
static方法可在一個(gè)類的范圍內(nèi)被相互間鎖定起來(lái),防止與static數(shù)據(jù)的接觸。<br>
注意如果想保護(hù)其他某些資源不被多個(gè)線程同時(shí)訪問(wèn),可以強(qiáng)制通過(guò)synchronized方訪問(wèn)那些資源。<br>
<br>
1. 計(jì)數(shù)器的同步<br>
裝備了這個(gè)新關(guān)鍵字后,我們能夠采取的方案就更靈活了:可以只為T(mén)woCounter中的方法簡(jiǎn)單地使用synchronized關(guān)鍵字。下面這個(gè)例子是對(duì)前例的改版,其中加入了新的關(guān)鍵字:<br>
<br>
775-778頁(yè)程序<br>
<br>
我們注意到無(wú)論run()還是synchTest()都是“同步的”。如果只同步其中的一個(gè)方法,那么另一個(gè)就可以自由忽視對(duì)象的鎖定,并可無(wú)礙地調(diào)用。所以必須記住一個(gè)重要的規(guī)則:對(duì)于訪問(wèn)某個(gè)關(guān)鍵共享資源的所有方法,都必須把它們?cè)O(shè)為synchronized,否則就不能正常地工作。<br>
?? 快捷鍵說(shuō)明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號(hào)
Ctrl + =
減小字號(hào)
Ctrl + -