?? 0120.htm
字號:
現在又遇到了一個新問題。Watcher2永遠都不能看到正在進行的事情,因為整個run()方法已設為“同步”。而且由于肯定要為每個對象運行run(),所以鎖永遠不能打開,而synchTest()永遠不會得到調用。之所以能看到這一結果,是因為accessCount根本沒有變化。<br>
為解決這個問題,我們能采取的一個辦法是只將run()中的一部分代碼隔離出來。想用這個辦法隔離出來的那部分代碼叫作“關鍵區域”,而且要用不同的方式來使用synchronized關鍵字,以設置一個關鍵區域。Java通過“同步塊”提供對關鍵區域的支持;這一次,我們用synchronized關鍵字指出對象的鎖用于對其中封閉的代碼進行同步。如下所示:<br>
<br>
779頁中程序<br>
<br>
在能進入同步塊之前,必須在synchObject上取得鎖。如果已有其他線程取得了這把鎖,塊便不能進入,必須等候那把鎖被釋放。<br>
可從整個run()中刪除synchronized關鍵字,換成用一個同步塊包圍兩個關鍵行,從而完成對Sharing2例子的修改。但什么對象應作為鎖來使用呢?那個對象已由synchTest()標記出來了——也就是當前對象(this)!所以修改過的run()方法象下面這個樣子:<br>
<br>
779頁下程序<br>
<br>
這是必須對Sharing2.java作出的唯一修改,我們會看到盡管兩個計數器永遠不會脫離同步(取決于允許Watcher什么時候檢查它們),但在run()執行期間,仍然向Watcher提供了足夠的訪問權限。<br>
當然,所有同步都取決于程序員是否勤奮:要訪問共享資源的每一部分代碼都必須封裝到一個適當的同步塊里。<br>
<br>
2. 同步的效率<br>
由于要為同樣的數據編寫兩個方法,所以無論如何都不會給人留下效率很高的印象。看來似乎更好的一種做法是將所有方法都設為自動同步,并完全消除synchronized關鍵字(當然,含有synchronized
run()的例子顯示出這樣做是很不通的)。但它也揭示出獲取一把鎖并非一種“廉價”方案——為一次方法調用付出的代價(進入和退出方法,不執行方法主體)至少要累加到四倍,而且根據我們的具體現方案,這一代價還有可能變得更高。所以假如已知一個方法不會造成沖突,最明智的做法便是撤消其中的synchronized關鍵字。<br>
<br>
14.2.3 回顧Java Beans<br>
我們現在已理解了同步,接著可換從另一個角度來考察Java Beans。無論什么時候創建了一個Bean,就必須假定它要在一個多線程的環境中運行。這意味著:<br>
(1) 只要可行,Bean的所有公共方法都應同步。當然,這也帶來了“同步”在運行期間的開銷。若特別在意這個問題,在關鍵區域中不會造成問題的方法就可保留為“不同步”,但注意這通常都不是十分容易判斷。有資格的方法傾向于規模很小(如下例的getCircleSize())以及/或者“微小”。也就是說,這個方法調用在如此少的代碼片里執行,以至于在執行期間對象不能改變。如果將這種方法設為“不同步”,可能對程序的執行速度不會有明顯的影響。可能也將一個Bean的所有public方法都設為synchronized,并只有在保證特別必要、而且會造成一個差異的情況下,才將synchronized關鍵字刪去。<br>
(2)
如果將一個多造型事件送給一系列對那個事件感興趣的“聽眾”,必須假在列表中移動的時候可以添加或者刪除。<br>
<br>
第一點很容易處理,但第二點需要考慮更多的東西。讓我們以前一章提供的BangBean.java為例。在那個例子中,我們忽略了synchronized關鍵字(那時還沒有引入呢),并將造型設為單造型,從而回避了多線程的問題。在下面這個修改過的版本中,我們使其能在多線程環境中工作,并為事件采用了多造型技術:<br>
<br>
781-784頁程序<br>
<br>
很容易就可以為方法添加synchronized。但注意在addActionListener()和removeActionListener()中,現在添加了ActionListener,并從一個Vector中移去,所以能夠根據自己愿望使用任意多個。<br>
我們注意到,notifyListeners()方法并未設為“同步”。可從多個線程中發出對這個方法的調用。另外,在對notifyListeners()調用的中途,也可能發出對addActionListener()和removeActionListener()的調用。這顯然會造成問題,因為它否定了Vector
actionListeners。為緩解這個問題,我們在一個synchronized從句中“克隆”了Vector,并對克隆進行了否定。這樣便可在不影響notifyListeners()的前提下,對Vector進行操縱。<br>
paint()方法也沒有設為“同步”。與單純地添加自己的方法相比,決定是否對過載的方法進行同步要困難得多。在這個例子中,無論paint()是否“同步”,它似乎都能正常地工作。但必須考慮的問題包括:<br>
(1)
方法會在對象內部修改“關鍵”變量的狀態嗎?為判斷一個變量是否“關鍵”,必須知道它是否會被程序中的其他線程讀取或設置(就目前的情況看,讀取或設置幾乎肯定是通過“同步”方法進行的,所以可以只對它們進行檢查)。對paint()的情況來說,不會發生任何修改。<br>
(2)
方法要以這些“關鍵”變量的狀態為基礎嗎?如果一個“同步”方法修改了一個變量,而我們的方法要用到這個變量,那么一般都愿意把自己的方法也設為“同步”。基于這一前提,大家可觀察到cSize由“同步”方法進行了修改,所以paint()應當是“同步”的。但在這里,我們可以問:“假如cSize在paint()執行期間發生了變化,會發生的最糟糕的事情是什么呢?”如果發現情況不算太壞,而且僅僅是暫時的效果,那么最好保持paint()的“不同步”狀態,以避免同步方法調用帶來的額外開銷。<br>
(3) 要留意的第三條線索是paint()基礎類版本是否“同步”,在這里它不是同步的。這并不是一個非常嚴格的參數,僅僅是一條“線索”。比如在目前的情況下,通過同步方法(好cSize)改變的一個字段已合成到paint()公式里,而且可能已改變了情況。但請注意,synchronized不能繼承——也就是說,假如一個方法在基礎類中是“同步”的,那么在衍生類過載版本中,它不會自動進入“同步”狀態。<br>
TestBangBean2中的測試代碼已在前一章的基礎上進行了修改,已在其中加入了額外的“聽眾”,從而演示了BangBean2的多造型能力。<br>
<br>
14.3 堵塞<br>
一個線程可以有四種狀態:<br>
(1) 新(New):線程對象已經創建,但尚未啟動,所以不可運行。<br>
(2) 可運行(Runnable):意味著一旦時間分片機制有空閑的CPU周期提供給一個線程,那個線程便可立即開始運行。因此,線程可能在、也可能不在運行當中,但一旦條件許可,沒有什么能阻止它的運行——它既沒有“死”掉,也未被“堵塞”。<br>
(3) 死(Dead):從自己的run()方法中返回后,一個線程便已“死”掉。亦可調用stop()令其死掉,但會產生一個違例——屬于Error的一個子類(也就是說,我們通常不捕獲它)。記住一個違例的“擲”出應當是一個特殊事件,而不是正常程序運行的一部分。所以不建議你使用stop()(在Java
1.2則是堅決反對)。另外還有一個destroy()方法(它永遠不會實現),應該盡可能地避免調用它,因為它非常武斷,根本不會解除對象的鎖定。<br>
(4) 堵塞(Blocked):線程可以運行,但有某種東西阻礙了它。若線程處于堵塞狀態,調度機制可以簡單地跳過它,不給它分配任何CPU時間。除非線程再次進入“可運行”狀態,否則不會采取任何操作。<br>
<br>
14.3.1 為何會堵塞<br>
堵塞狀態是前述四種狀態中最有趣的,值得我們作進一步的探討。線程被堵塞可能是由下述五方面的原因造成的:<br>
(1) 調用sleep(毫秒數),使線程進入“睡眠”狀態。在規定的時間內,這個線程是不會運行的。<br>
(2) 用suspend()暫停了線程的執行。除非線程收到resume()消息,否則不會返回“可運行”狀態。<br>
(3) 用wait()暫停了線程的執行。除非線程收到nofify()或者notifyAll()消息,否則不會變成“可運行”(是的,這看起來同原因2非常相象,但有一個明顯的區別是我們馬上要揭示的)。<br>
(4) 線程正在等候一些IO(輸入輸出)操作完成。<br>
(5)
線程試圖調用另一個對象的“同步”方法,但那個對象處于鎖定狀態,暫時無法使用。<br>
<br>
亦可調用yield()(Thread類的一個方法)自動放棄CPU,以便其他線程能夠運行。然而,假如調度機制覺得我們的線程已擁有足夠的時間,并跳轉到另一個線程,就會發生同樣的事情。也就是說,沒有什么能防止調度機制重新啟動我們的線程。線程被堵塞后,便有一些原因造成它不能繼續運行。<br>
下面這個例子展示了進入堵塞狀態的全部五種途徑。它們全都存在于名為Blocking.java的一個文件中,但在這兒采用散落的片斷進行解釋(大家可注意到片斷前后的“Continued”以及“Continuing”標志。利用第17章介紹的工具,可將這些片斷連結到一起)。首先讓我們看看基本的框架:<br>
<br>
786-787頁程序<br>
<br>
Blockable類打算成為本例所有類的一個基礎類。一個Blockable對象包含了一個名為state的TextField(文本字段),用于顯示出對象有關的信息。用于顯示這些信息的方法叫作update()。我們發現它用getClass.getName()來產生類名,而不是僅僅把它打印出來;這是由于update(0不知道自己為其調用的那個類的準確名字,因為那個類是從Blockable衍生出來的。<br>
在Blockable中,變動指示符是一個int i;衍生類的run()方法會為其增值。<br>
針對每個Bloackable對象,都會啟動Peeker類的一個線程。Peeker的任務是調用read()方法,檢查與自己關聯的Blockable對象,看看i是否發生了變化,最后用它的status文本字段報告檢查結果。注意read()和update()都是同步的,要求對象的鎖定能自由解除,這一點非常重要。<br>
<br>
1. 睡眠<br>
這個程序的第一項測試是用sleep()作出的:<br>
<br>
788-789頁程序<br>
<br>
在Sleeper1中,整個run()方法都是同步的。我們可看到與這個對象關聯在一起的Peeker可以正常運行,直到我們啟動線程為止,隨后Peeker便會完全停止。這正是“堵塞”的一種形式:因為Sleeper1.run()是同步的,而且一旦線程啟動,它就肯定在run()內部,方法永遠不會放棄對象鎖定,造成Peeker線程的堵塞。<br>
Sleeper2通過設置不同步的運行,提供了一種解決方案。只有change()方法才是同步的,所以盡管run()位于sleep()內部,Peeker仍然能訪問自己需要的同步方法——read()。在這里,我們可看到在啟動了Sleeper2線程以后,Peeker會持續運行下去。<br>
<br>
2. 暫停和恢復<br>
這個例子接下來的一部分引入了“掛起”或者“暫停”(Suspend)的概述。Thread類提供了一個名為suspend()的方法,可臨時中止線程;以及一個名為resume()的方法,用于從暫停處開始恢復線程的執行。顯然,我們可以推斷出resume()是由暫停線程外部的某個線程調用的。在這種情況下,需要用到一個名為Resumer(恢復器)的獨立類。演示暫停/恢復過程的每個類都有一個相關的恢復器。如下所示:<br>
<br>
789-790頁程序<br>
<br>
SuspendResume1也提供了一個同步的run()方法。同樣地,當我們啟動這個線程以后,就會發現與它關聯的Peeker進入“堵塞”狀態,等候對象鎖被釋放,但那永遠不會發生。和往常一樣,這個問題在SuspendResume2里得到了解決,它并不同步整個run()方法,而是采用了一個單獨的同步change()方法。<br>
對于Java 1.2,大家應注意suspend()和resume()已獲得強烈反對,因為suspend()包含了對象鎖,所以極易出現“死鎖”現象。換言之,很容易就會看到許多被鎖住的對象在傻乎乎地等待對方。這會造成整個應用程序的“凝固”。盡管在一些老程序中還能看到它們的蹤跡,但在你寫自己的程序時,無論如何都應避免。本章稍后就會講述正確的方案是什么。<br>
<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -