?? 0120.htm
字號:
3. 等待和通知<br>
通過前兩個例子的實踐,我們知道無論sleep()還是suspend()都不會在自己被調用的時候解除鎖定。需要用到對象鎖時,請務必注意這個問題。在另一方面,wait()方法在被調用時卻會解除鎖定,這意味著可在執行wait()期間調用線程對象中的其他同步方法。但在接著的兩個類中,我們看到run()方法都是“同步”的。在wait()期間,Peeker仍然擁有對同步方法的完全訪問權限。這是由于wait()在掛起內部調用的方法時,會解除對象的鎖定。<br>
我們也可以看到wait()的兩種形式。第一種形式采用一個以毫秒為單位的參數,它具有與sleep()中相同的含義:暫停這一段規定時間。區別在于在wait()中,對象鎖已被解除,而且能夠自由地退出wait(),因為一個notify()可強行使時間流逝。<br>
第二種形式不采用任何參數,這意味著wait()會持續執行,直到notify()介入為止。而且在一段時間以后,不會自行中止。<br>
wait()和notify()比較特別的一個地方是這兩個方法都屬于基礎類Object的一部分,不象sleep(),suspend()以及resume()那樣屬于Thread的一部分。盡管這表面看有點兒奇怪——居然讓專門進行線程處理的東西成為通用基礎類的一部分——但仔細想想又會釋然,因為它們操縱的對象鎖也屬于每個對象的一部分。因此,我們可將一個wait()置入任何同步方法內部,無論在那個類里是否準備進行涉及線程的處理。事實上,我們能調用wait()的唯一地方是在一個同步的方法或代碼塊內部。若在一個不同步的方法內調用wait()或者notify(),盡管程序仍然會編譯,但在運行它的時候,就會得到一個IllegalMonitorStateException(非法監視器狀態違例),而且會出現多少有點莫名其妙的一條消息:“current
thread not owner”(當前線程不是所有人”。注意sleep(),suspend()以及resume()都能在不同步的方法內調用,因為它們不需要對鎖定進行操作。<br>
只能為自己的鎖定調用wait()和notify()。同樣地,仍然可以編譯那些試圖使用錯誤鎖定的代碼,但和往常一樣會產生同樣的IllegalMonitorStateException違例。我們沒辦法用其他人的對象鎖來愚弄系統,但可要求另一個對象執行相應的操作,對它自己的鎖進行操作。所以一種做法是創建一個同步方法,令其為自己的對象調用notify()。但在Notifier中,我們會看到一個同步方法內部的notify():<br>
<br>
792頁上程序<br>
<br>
其中,wn2是類型為WaitNotify2的對象。盡管并不屬于WaitNotify2的一部分,這個方法仍然獲得了wn2對象的鎖定。在這個時候,它為wn2調用notify()是合法的,不會得到IllegalMonitorStateException違例。<br>
<br>
792-793頁程序<br>
<br>
若必須等候其他某些條件(從線程外部加以控制)發生變化,同時又不想在線程內一直傻乎乎地等下去,一般就需要用到wait()。wait()允許我們將線程置入“睡眠”狀態,同時又“積極”地等待條件發生改變。而且只有在一個notify()或notifyAll()發生變化的時候,線程才會被喚醒,并檢查條件是否有變。因此,我們認為它提供了在線程間進行同步的一種手段。<br>
<br>
4. IO堵塞<br>
若一個數據流必須等候一些IO活動,便會自動進入“堵塞”狀態。在本例下面列出的部分中,有兩個類協同通用的Reader以及Writer對象工作(使用Java
1.1的流)。但在測試模型中,會設置一個管道化的數據流,使兩個線程相互間能安全地傳遞數據(這正是使用管道流的目的)。<br>
Sender將數據置入Writer,并“睡眠”隨機長短的時間。然而,Receiver本身并沒有包括sleep(),suspend()或者wait()方法。但在執行read()的時候,如果沒有數據存在,它會自動進入“堵塞”狀態。如下所示:<br>
<br>
793-794頁程序<br>
<br>
這兩個類也將信息送入自己的state字段,并修改i值,使Peeker知道線程仍在運行。<br>
<br>
5. 測試<br>
令人驚訝的是,主要的程序片(Applet)類非常簡單,這是大多數工作都已置入Blockable框架的緣故。大概地說,我們創建了一個由Blockable對象構成的數組。而且由于每個對象都是一個線程,所以在按下“start”按鈕后,它們會采取自己的行動。還有另一個按鈕和actionPerformed()從句,用于中止所有Peeker對象。由于Java
1.2“反對”使用Thread的stop()方法,所以可考慮采用這種折衷形式的中止方式。<br>
為了在Sender和Receiver之間建立一個連接,我們創建了一個PipedWriter和一個PipedReader。注意PipedReader
in必須通過一個構建器參數同PipedWriterout連接起來。在那以后,我們在out內放進去的所有東西都可從in中提取出來——似乎那些東西是通過一個“管道”傳輸過去的。隨后將in和out對象分別傳遞給Receiver和Sender構建器;后者將它們當作任意類型的Reader和Writer看待(也就是說,它們被“上溯”造型了)。<br>
Blockable句柄b的數組在定義之初并未得到初始化,因為管道化的數據流是不可在定義前設置好的(對try塊的需要將成為障礙):<br>
<br>
795-796頁程序<br>
<br>
在init()中,注意循環會遍歷整個數組,并為頁添加state和peeker.status文本字段。<br>
首次創建好Blockable線程以后,每個這樣的線程都會自動創建并啟動自己的Peeker。所以我們會看到各個Peeker都在Blockable線程啟動之前運行起來。這一點非常重要,因為在Blockable線程啟動的時候,部分Peeker會被堵塞,并停止運行。弄懂這一點,將有助于我們加深對“堵塞”這一概念的認識。<br>
<br>
14.3.2 死鎖<br>
由于線程可能進入堵塞狀態,而且由于對象可能擁有“同步”方法——除非同步鎖定被解除,否則線程不能訪問那個對象——所以一個線程完全可能等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候”鏈最可怕的情形就是進入封閉狀態——最后那個對象等候的是第一個對象!此時,所有線程都會陷入無休止的相互等待狀態,大家都動彈不得。我們將這種情況稱為“死鎖”。盡管這種情況并非經常出現,但一旦碰到,程序的調試將變得異常艱難。<br>
就語言本身來說,尚未直接提供防止死鎖的幫助措施,需要我們通過謹慎的設計來避免。如果有誰需要調試一個死鎖的程序,他是沒有任何竅門可用的。<br>
<br>
1. Java 1.2對stop(),suspend(),resume()以及destroy()的反對<br>
為減少出現死鎖的可能,Java 1.2作出的一項貢獻是“反對”使用Thread的stop(),suspend(),resume()以及destroy()方法。<br>
之所以反對使用stop(),是因為它不安全。它會解除由線程獲取的所有鎖定,而且如果對象處于一種不連貫狀態(“被破壞”),那么其他線程能在那種狀態下檢查和修改它們。結果便造成了一種微妙的局面,我們很難檢查出真正的問題所在。所以應盡量避免使用stop(),應該采用Blocking.java那樣的方法,用一個標志告訴線程什么時候通過退出自己的run()方法來中止自己的執行。<br>
如果一個線程被堵塞,比如在它等候輸入的時候,那么一般都不能象在Blocking.java中那樣輪詢一個標志。但在這些情況下,我們仍然不該使用stop(),而應換用由Thread提供的interrupt()方法,以便中止并退出堵塞的代碼。<br>
<br>
797-798頁程序<br>
<br>
Blocked.run()內部的wait()會產生堵塞的線程。當我們按下按鈕以后,blocked(堵塞)的句柄就會設為null,使垃圾收集器能夠將其清除,然后調用對象的interrupt()方法。如果是首次按下按鈕,我們會看到線程正常退出。但在沒有可供“殺死”的線程以后,看到的便只是按鈕被按下而已。<br>
suspend()和resume()方法天生容易發生死鎖。調用suspend()的時候,目標線程會停下來,但卻仍然持有在這之前獲得的鎖定。此時,其他任何線程都不能訪問鎖定的資源,除非被“掛起”的線程恢復運行。對任何線程來說,如果它們想恢復目標線程,同時又試圖使用任何一個鎖定的資源,就會造成令人難堪的死鎖。所以我們不應該使用suspend()和resume(),而應在自己的Thread類中置入一個標志,指出線程應該活動還是掛起。若標志指出線程應該掛起,便用wait()命其進入等待狀態。若標志指出線程應當恢復,則用一個notify()重新啟動線程。我們可以修改前面的Counter2.java來實際體驗一番。盡管兩個版本的效果是差不多的,但大家會注意到代碼的組織結構發生了很大的變化——為所有“聽眾”都使用了匿名的內部類,而且Thread是一個內部類。這使得程序的編寫稍微方便一些,因為它取消了Counter2.java中一些額外的記錄工作。<br>
<br>
799-801頁程序<br>
<br>
Suspendable中的suspended(已掛起)標志用于開關“掛起”或者“暫停”狀態。為掛起一個線程,只需調用fauxSuspend()將標志設為true(真)即可。對標志狀態的偵測是在run()內進行的。就象本章早些時候提到的那樣,wait()必須設為“同步”(synchronized),使其能夠使用對象鎖。在fauxResume()中,suspended標志被設為false(假),并調用notify()——由于這會在一個“同步”從句中喚醒wait(),所以fauxResume()方法也必須同步,使其能在調用notify()之前取得對象鎖(這樣一來,對象鎖可由要喚醍的那個wait()使用)。如果遵照本程序展示的樣式,可以避免使用wait()和notify()。<br>
Thread的destroy()方法根本沒有實現;它類似一個根本不能恢復的suspend(),所以會發生與suspend()一樣的死鎖問題。然而,這一方法沒有得到明確的“反對”,也許會在Java以后的版本(1.2版以后)實現,用于一些可以承受死鎖危險的特殊場合。<br>
大家可能會奇怪當初為什么要實現這些現在又被“反對”的方法。之所以會出現這種情況,大概是由于Sun公司主要讓技術人員來決定對語言的改動,而不是那些市場銷售人員。通常,技術人員比搞銷售的更能理解語言的實質。當初犯下了錯誤以后,也能較為理智地正視它們。這意味著Java能夠繼續進步,即便這使Java程序員多少感到有些不便。就我自己來說,寧愿面對這些不便之處,也不愿看到語言停滯不前。<br>
<br>
14.4 優先級<br>
線程的優先級(Priority)告訴調試程序該線程的重要程度有多大。如果有大量線程都被堵塞,都在等候運行,調試程序會首先運行具有最高優先級的那個線程。然而,這并不表示優先級較低的線程不會運行(換言之,不會因為存在優先級而導致死鎖)。若線程的優先級較低,只不過表示它被準許運行的機會小一些而已。<br>
可用getPriority()方法讀取一個線程的優先級,并用setPriority()改變它。在下面這個程序片中,大家會發現計數器的計數速度慢了下來,因為它們關聯的線程分配了較低的優先級:<br>
<br>
802-805頁程序<br>
<br>
Ticker采用本章前面構造好的形式,但有一個額外的TextField(文本字段),用于顯示線程的優先級;以及兩個額外的按鈕,用于人為提高及降低優先級。<br>
也要注意yield()的用法,它將控制權自動返回給調試程序(機制)。若不進行這樣的處理,多線程機制仍會工作,但我們會發現它的運行速度慢了下來(試試刪去對yield()的調用)。亦可調用sleep(),但假若那樣做,計數頻率就會改由sleep()的持續時間控制,而不是優先級。<br>
Counter5中的init()創建了由10個Ticker2構成的一個數組;它們的按鈕以及輸入字段(文本字段)由Ticker2構建器置入窗體。Counter5增加了新的按鈕,用于啟動一切,以及用于提高和降低線程組的最大優先級。除此以外,還有一些標簽用于顯示一個線程可以采用的最大及最小優先級;以及一個特殊的文本字段,用于顯示線程組的最大優先級(在下一節里,我們將全面討論線程組的問題)。最后,父線程組的優先級也作為標簽顯示出來。<br>
按下“up”(上)或“down”(下)按鈕的時候,會先取得Ticker2當前的優先級,然后相應地提高或者降低。<br>
運行該程序時,我們可注意到幾件事情。首先,線程組的默認優先級是5。即使在啟動線程之前(或者在創建線程之前,這要求對代碼進行適當的修改)將最大優先級降到5以下,每個線程都會有一個5的默認優先級。<br>
最簡單的測試是獲取一個計數器,將它的優先級降低至1,此時應觀察到它的計數頻率顯著放慢。現在試著再次提高優先級,可以升高回線程組的優先級,但不能再高了。現在將線程組的優先級降低兩次。線程的優先級不會改變,但假若試圖提高或者降低它,就會發現這個優先級自動變成線程組的優先級。此外,新線程仍然具有一個默認優先級,即使它比組的優先級還要高(換句話說,不要指望利用組優先級來防止新線程擁有比現有的更高的優先級)。<br>
最后,試著提高組的最大優先級。可以發現,這樣做是沒有效果的。我們只能減少線程組的最大優先級,而不能增大它。<br>
<br>
14.4.1 線程組<br>
所有線程都隸屬于一個線程組。那可以是一個默認線程組,亦可是一個創建線程時明確指定的組。在創建之初,線程被限制到一個組里,而且不能改變到一個不同的組。每個應用都至少有一個線程從屬于系統線程組。若創建多個線程而不指定一個組,它們就會自動歸屬于系統線程組。<br>
線程組也必須從屬于其他線程組。必須在構建器里指定新線程組從屬于哪個線程組。若在創建一個線程組的時候沒有指定它的歸屬,則同樣會自動成為系統線程組的一名屬下。因此,一個應用程序中的所有線程組最終都會將系統線程組作為自己的“父”。<br>
之所以要提出“線程組”的概念,很難從字面上找到原因。這多少為我們討論的主題帶來了一些混亂。一般地說,我們認為是由于“安全”或者“保密”方面的理由才使用線程組的。根據Arnold和Gosling的說法:“線程組中的線程可以修改組內的其他線程,包括那些位于分層結構最深處的。一個線程不能修改位于自己所在組或者下屬組之外的任何線程”(注釋①)。然而,我們很難判斷“修改”在這兒的具體含義是什么。下面這個例子展示了位于一個“葉子組”內的線程能修改它所在線程組樹的所有線程的優先級,同時還能為這個“樹”內的所有線程都調用一個方法。<br>
<br>
①:《The Java Programming Language》第179頁。該書由Arnold和Jams Gosling編著,Addison-Wesley于1996年出版<br>
<br>
807-808頁程序<br>
<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -