關鍵字:Observer Pattern、Java Thread、Java Swing Application
1 近來的閱讀
近來寒暑不常,希自珍慰。武漢天氣不是狂冷,就是狂熱,不時還給我整個雪花,就差冰雹了。
自己做的事吧,也沒有什么勁兒。看看自己喜歡的東西,等著希望中的學校能給我offers(是復數),看著自己想去又不想去的公司的未來同事在群里面幻想未來的樣子,別操你大爺了,都成人這么久了,咋就還不了解這個世道呢?你給老板賺十塊錢,老板自然會給你一塊。如果老板的周圍會賺錢的人不多,你還可以嘗試吆喝更高的價錢。就這么個事,幻想有個鳥用。還不如靜下心來好好學點兒東西,想想將來怎么多多給老板賺錢。無論以后打工還是自己當老板,路都得這么走出來。
另外,就是思考的畢設的事情。畢竟,讓我立刻狠心的fuck掉這個學位也是不現實的。所以,繼續搞,搞一個題目叫做“基于復雜網絡的可度量防垃圾郵件系統”的論文加實驗系統。看名字很BT,可我覺得這已經是我覺得最有意思的畢設題目了。我一定要做出點東西來。
無聊看書加上要完成畢設的實驗系統,于是有了些動力,就產生了本篇小隨筆。權當備忘吧。
看著“觀察者模式”,琢磨琢磨,感覺跟別的模式有那么一點不一樣。但是又說不出來,感覺使用這個模式的環境,為多線程為多。然后,就轉而去看線程的書,看著線程的書,又發現畢設要用的Swing是學習理解線程很好的素材,而且Swing中的event-dispatch機制就是“觀察者模式”,于是又轉而去看講Swing的書及專欄文章。
到此,這3個東西是徹底扯在一起了。總結一下它們之間的關系:Swing是建立在event-dispatch機制及Swing Single Thread下的多線程GUI編程環境。當然,在很多情況下,你寫Swing也不大會用到(察覺)多線程(如我們的java課本中的Swing例子),但是,使用它的可能性已經大大增加了。而且要寫好Swing,不理解線程是很困難的。
2 Java Thread
2.1 操作系統及面向過程
對于我這個新手來講,使用多線程的機會不多。天天搞J2EE的應用開發,也根本用不上這個東西。但是,不理解你,不使用你,何談功力的提升呢。你不找我,我也要找你。
在面向對象的世界中,這個Thread讓我真的不好理解。它仿佛和我們自己寫的對象,JDK的別的API類庫都不一樣,咋一看,看不出它的“使用價值”。(我理解的對象都有很“明顯”的“使用價值”,呵呵)。
因為更多地談論“進程”、“線程”還是在操作系統層面,記得在OS課本中有好幾個章節都是講“進程”相關的知識:調度算法、調度隊列、調度優先級等等。后來上Unix和Linux的時候重點更是folk和進程間通訊(IPC)機制。但都是停留在理論理解,應付考試的階段。而且編寫的代碼也都是面向過程的風格,沒有“對象”的感覺。
Java能把OS層的概念加進來并予以實現,功能上很強,另外也給新手接觸底層編程的機會。但是,把OS層的線程概念及它的面向過程編寫風格與面向對象普遍的風格一比較,對Java Thread對象的理解就有點迷糊了。
2.2 從接口的本質思考
一般寫程序不會太關注main(),特別是J2EE的東西搞多了,更是把它給忘盡了。main是程序的開始,是JVM的主線程。線程是另一個main(這樣理解肯定不準確,但是有極大雷同),是欲并行執行的程序代碼序列。
在java中,如果在main之外,還要執行獨立并行的程序代碼序列,就要用到java線程了。方法也比較簡單:把獨立執行的代碼序列寫到一個Runnable對象的run方法中,然后在父進程(很有可能就是main線程)中start它(先要把Runnable變成Thread對象)就可以了。
接口描述的是一種功能,而不是事物本質(類來描述)。如果我們把“能獨立并行執行”當作一個對象的能力的話,把線程概念融入面向對象就容易多了。起碼我是這么理解的。
至于你是喜歡給已有的類添加一個這個功能(也就是讓已有的類來實現Runnable接口),還是另外專門寫一個類,專門封裝這種功能。就見仁見智了。正如我都喜歡把main獨立寫在一個控制類中,而不是放在功能類中一樣。
2.3 匿名的嵌套類
也正是因為線程的并行性本質,在我們的代碼中,那里要并行執行,我們就在這里開一個新的線程。而這時,為了最簡便,對代碼結構影響最小,最常見方式的就是開一個匿名的嵌套線程。
例子:如果doNoSequence()需要并行于執行doJob()的線程而獨立執行。
1
Void doJob()
{
2
3
doFirst();
6
7
//need to be excuted in concurrently.
9
doNoSequence();
11
13
doSecond();
14
15
}
16
就可以為它開一個匿名的嵌套線程。
1
Void doJob()
{
2
doFirst();
3
4
new Thread()
{
5
public void run()
{
6
doNoSequence();
7
}
8
}.start();
9
10
doSecond();
11
}
12
2.4 異步的本質,同步的需要
“獨立并行”的感覺一般開始都很好,至于以后,呵呵,不好說。
特別是當系統已經設計好了以后,在代碼中按上面的例子隨便開新線程。看似操作容易,但如果開的線程一多,相互之間的聯系一多,以后就很難理解、管理和維護(許多article作者的話,我沒有經驗)。
doNoSequence()現在就開始難受了,因為它發現它和主線程還有很多聯系,如和doFirst()有共同access的對象;還如doSecond()接受它返回的參數,一定要等它返回才能繼續執行(也就是doNoSequence()對于主線程是block的)。明顯,這里都需要“同步”。
當然,也不必絕望,只不過在代碼里面繼續添加更多的我不熟練的線程控制代碼。線程開的越多,線程有關的關鍵字和控制代碼就會出現的更多。最終的代碼也許會面目全非,越來越難看出核心邏輯了。線程代碼多了是小,但是核心的業務邏輯被淹沒了是不允許的。
最好的解決方法,當然就是在設計的時候,多考慮一下程序并行化的可能。把有可能并行的邏輯分開來寫。換句話講,也就是要增加并行程序內部的耦合性,降低并行程序之間的耦合性。以便將來并不并行都容易使用。
如要編寫既能異步執行,又能相對容易實現同步控制的代碼,不少人(當然,都是大牛人)推薦使用“觀察者模式”來重構你的系統。
3 觀察者模式
“觀察者模式(Observer Pattern)”是一個好東西,我以前實在是太孤陋寡聞了,竟然沒有好好的去了解這樣一個被廣泛使用的模式,它給我帶來很多的思考。通過它,使我更容易理解很多框架,資料,文章,代碼。因為,當我一看到listener,event,observer等詞語,就知道其設計原型為“觀察者模式”。
“觀察者模式”通過定義抽象主題[Abstrsct Subject](也叫消息源[Message Source]、事件源[Evnet Source]、被觀察者),抽象觀察者[Abstract Observer](更多的叫監聽者[listener]),及它們之間的所謂事件[Event](也叫消息[Message])來實現的。這里的事件其實是對被觀察者和觀察者之間傳遞信息的一個封裝,如果你覺得沒有必要封裝什么,那你用個String或int傳遞信息也是可以的。
自己再寫兩個類實現(Implement)上面兩個抽象類就完成了一個“觀察者模式”。
下面嘗試舉例說明,想必大家都經常被各種垃圾短信所困擾,一般這些短信都是推薦你去訂閱一些服務,如每日一警句,足球彩票預測,色情資訊等等。如果你按照上面的number回了信息,就等于訂閱了,也就要付錢了。為了防止你過分反感這個例子,影響敘述,那我就找個稍微有用那么一點的短信訂閱服務來舉例――“天氣預報”。
提供服務的“天氣預報提供商”就是模式中的“被觀察者”,而訂閱了服務的“訂閱者”就是模式中的“觀察者”。“天氣預報服務商啊,我已經訂閱你的天氣服務啦,我盯上你啦,你一旦有了明天的天氣情況就要及時通知我哦!”――這樣粗俗的描述或許會讓你更加容易理解,為什么天氣預報服務商被稱為“被觀察者”,而我們被稱為“觀察者”,而我們之間傳遞的信息就是天氣情況。
例子的類圖:
模擬訂閱及發送天氣預報的代碼:
1
package gs.blogExample.weatherService;
2
3
public class SimulatedEnvironment
{
4
public static void main(String[] args)
{
5
6
//定義天氣服務提供商(模式角色:被觀察者)
7
WeatherServiceProvider myProvider = new WeatherServiceProvider();
8
9
//定義兩個天氣服務訂閱者(模式角色:觀察者)
10
Housewife hw = new Housewife();
11
BusDriver bd = new BusDriver();
12
13
//他們訂閱天氣服務
14
myProvider.addServiceListener(hw);
15
myProvider.addServiceListener(bd);
16
17
System.out.println("<<Date:2005-3-12>>");
18
//天氣服務提供商得到了新的天氣信息,武漢今天9度
19
//只要天氣服務商得到了任何一個城市的新天氣,就會通知訂閱者
20
myProvider.setNewWeather(new Weather("WUHAN",9,10));
21
22
//housewife退訂天氣服務
23
myProvider.removeServiceListener(hw);
24
25
System.out.println("<<Date:2005-3-13>>");
26
//第二天,天氣供應商又得到了新的信息,武漢今天35度
27
//武漢一天之間從9度變35度,是可信的
28
myProvider.setNewWeather(new Weather("WUHAN",35,36));
29
}
30
}
31
模擬輸出的結果如下:
<<Date:2005-3-12>>
Housewife receiving weatherInfo begins..........
Housewife said: “so cool,let's make huoguo!!”
Housewife receiving weatherInfo ended..........
BusDriver receiving weatherInfo begins..........
BusDriver said: “fine day,nothing to do.”
BusDriver receiving weatherInfo ended..........
<<Date:2005-3-13>>
BusDriver receiving weatherInfo begins..........
BusDriver said: “so hot,open air condition!!”
BusDriver receiving weatherInfo ended..........
說明:訂閱者接到天氣預報后的行為就是“隨便打印一句感受”。也就是說,例子中訂閱者Print出一句話就代表他收到天氣預報了。
從上面的輸出結果可以看到,當Housewife在2005-3-12號退訂了天氣服務。的確,第二天天氣預報提供商就沒有再給它提供服務了。
繼續拓展這個系統,使之提供另一種服務――“足球貼士服務”,根據“觀察者模式”的角色,添加“足球貼士提供商”(被觀察者)、“足球貼士訂閱者”(觀察者)和事件類“足球貼士事件”,另還有一個輔助描述具體足球貼士信息的類“足球貼士信息”。
繼續類圖:
模擬發送接受天氣預報和足球貼士的代碼:
1
package gs.blogExample.weatherService;
2
3
public class SimulatedEnvironment
{
4
public static void main(String[] args)
{
5
6
//定義天氣服務提供商(模式角色:被觀察者)
7
WeatherServiceProvider weatherServiceProvider = new WeatherServiceProvider();
8
9
//定義天氣服務提供商(模式角色:被觀察者)
10
SoccerTipServiceProvider soccerTipServiceProvider = new SoccerTipServiceProvider();
11
12
//定義兩個天氣服務訂閱者(模式角色:觀察者)
13
Housewife hw = new Housewife();
14
BusDriver bd = new BusDriver();
15
16
//hw、bd都訂閱天氣服務,bd還訂閱了足球貼士服務
17
weatherServiceProvider.addServiceListener(hw);
18
weatherServiceProvider.addServiceListener(bd);
19
soccerTipServiceProvider.addServiceListener(bd);
20
21
System.out.println("<<Date:2005-3-12>>");
22
//天氣服務提供商得到了新的天氣信息,武漢今天9度
23
//足球貼士提供商得到了新的預測信息,MAN VS ASL 預測是1
24
weatherServiceProvider.setNewWeather(new Weather("WUHAN",9,10));
25
soccerTipServiceProvider.setNewSoccerTip(new SoccerTip("MAN VS ASL","2005-4-5","1"));
26
27
//housewife退訂天氣服務
28
weatherServiceProvider.removeServiceListener(hw);
29
30
System.out.println("<<Date:2005-3-13>>");
31
//第二天,天氣供應商又得到了新的信息,武漢今天35度
32
weatherServiceProvider.setNewWeather(new Weather("WUHAN",35,36));
33
}
34
}
35
模擬輸出的結果如下:
<<Date:2005-3-12>>
Housewife receiving weatherInfo begins..........
Housewife said: "so cool,let's make huoguo!!"
Housewife receiving weatherInfo ended..........
BusDriver receiving weatherInfo begins..........
BusDriver said: "fine day,nothing to do."
BusDriver receiving weatherInfo ended..........
BusDriver receiving soccerTip begins..........
BusDriver said: "I am about to buy soccer lottery!!!I want to be rich!"
BusDriver receiving soccerTip ended..........
<<Date:2005-3-13>>
BusDriver receiving weatherInfo begins..........
BusDriver said: "so hot,open air condition!!"
BusDriver receiving weatherInfo ended..........
源碼下載(JBuilder Project)
3.1 天生的異步模型
1
private void sendWeatherService()
{
2
3
WeatherEvent we = new WeatherEvent(this.cityWeather);
4
5
Vector cloneListenerList = (Vector) getListenerList().clone();
6
Iterator iter = cloneListenerList.iterator();
7
while (iter.hasNext())
{
8
WeatherServiceListener listener =
9
(WeatherServiceListener) iter.next();
10
listener.WeatherArrived(we);
11
}
12
13
}
14
繼續上面的例子,天氣預報提供商為訂閱者發送天氣預報的行為被封裝在sendWeatherService方法中,而天氣預報訂閱者接受到天氣預報信息后的行為被封裝在WeatherArrived方法中。
從上面天氣預報提供商的sendWeatherService方法的代碼可以看到,提供商會遍歷所有WeatherListener,然后通過調用它們的WeatherArrived方法來向它們傳遞WeatherEvent事件。
在這里,兩個方法是在一個線程中運行的,也就是說,對于天氣提供商來講,如果第一個listener的WeatherArrived方法沒有返回,那它就沒有辦法通知第二個listener。
具體到我們這個例子,我們是先通知Housewife再通知BusDriver的(由于遍歷順序的原因),也就是說是先執行Housewife的WeatherArrived方法,再執行BusDriver的WeatherArrived方法。試想,如果Housewife的WeatherArrived方法中要做的事情很多(往往家庭主婦就是如此),長期得不到返回,那么BusDriver的WeatherArrived方法就長期得不到執行,也就是說BusDriver長期得不到天氣消息。對于天氣這種時效性很強的信息,很明顯,這樣不行。是Bug!
自然的,可以運用Java Thraed,考慮把listener們的WeatherArrived并行化,放在不同的線程里去執行。這樣,每當通知第一個listener都不會阻塞通知下一個listener。
的確,無論用不用觀察者模式,你加幾行java線程代碼都可以實現并行化。只是,“觀察者模式”無論在構思理解層面上,還是代碼結構上,都特別容易向并行化過渡。
為什么這面說?一句話講:“觀察者模式”中的“被觀察者角色”和“觀察者角色”本生內在就有強烈的并行性特征。擁有“觀察者模式”的系統一般都有這個特性,而有這個特性的系統也一般也可以設計成“觀察者模式”。
3.2 設計并行系統的好方法
用Messge機制或Event機制(以“觀察者模式”原型)來設計系統,可以更好的適應一旦程序有多線程的需要。因為在“觀察者模式”中,我們在定義角色及事件時,其實它們已有并行的特征(也就是多線程的可能)。
這樣可以讓我們設計系統的時候,暫時不把主要的腦神經拿去考慮線程的復雜問題,放心的先把系統涉及的對象設計出來。而后來一旦有了線程化的需要,可以相對方便、清晰的添加,也不會出現太“拗眼”的代碼群。
結論還是,有利于系統的清晰,維護的方便。
在Swing應用程序的設計中,就有人這樣設計,并且把它上升為一種通用方法,可參見下面的Event-Dispatch方法章節。
4 Java Swing
Swing是Java的GUI類庫。因此,Java的桌面應用程序,也被稱為Swing應用程序。它的優點是Java的優點:“Write once,Run anywhere”;它的缺點也是Java的缺點:“性能問題要命”。
4.1 Swing單線程模型
桌面應用程序生命周期可以概括如下:
STEP 1:初始化界面;
STEP 2:等待你的event,主要還是鼠標和鍵盤event;
STEP 3:event一到,執行相應的業務邏輯代碼;
STEP 4:根據結果,刷新界面,用戶看到;
STEP 5:然后,又到步驟2,直到你退出。
所謂Swing單線程模型的意思就是,所有和“界面”有關系的代碼都在Swing規定的線程(稱為Swing線程)里按照順序被執行,不可以用自己定義的線程跟“界面”有任何的接觸(例外就不贅述了)。這里的“界面”指的是Jcomponent的子類,容器、按鈕、畫布都屬于界面的范圍。這里的“和界面有關系”包括event響應、repaint、改變屬性等等。
簡單來說,就是不能用自己的線程控制這些Jcomponent對象啦。
真衰,這么干的原因還不是讓Java的桌面程序不至于太慢,結果所有(也有例外)Swing的類都被設計成“非”threadsafe的,因此也只能讓一個線程(Swing線程)訪問了。
4.2 Long-Running Event Callbacks
Swing編程中最大的一個麻煩就是處理“長時間事件響應”。對照上面生命周期的step3(event一到,執行相應的業務邏輯代碼),“長時間事件響應”的意思就是Step3執行的時間太長了,方法長期返回不了,以至把整個Swing線程的生命周期阻塞了,用戶看起來就像死機了。
而一般這種情況,這個“長時間事件響應”都是在Swing線程中執行的,因此阻塞了才會這么大件事。
那就把它移出Swing線程吧,讓它自己到另外的線程里面去做。這樣即使你阻塞了,也不至于阻塞Swing線程這個事關給用戶感覺的線程。
但問題還沒有完,盡管上面的step3,也就是這個“長時間事件響應”的確對Swing線程沒有影響了,Swing線程不會被阻塞了。但是在新線程(稱為N進程)里,程序到了step4(根據結果,刷新界面,用戶看到), 它要根據step3的結果去刷新界面,把結果顯示給用戶。而根據Swing的單線程模型,N進程是沒有權利去接觸界面對象的,只有Swing線程才有這個權利。所以,身處N進程的step4又必須被放回到Swing線程里去執行。
這樣,來來回回,麻麻煩煩,把我Swing暈了,怪不得叫Swing,真是“忽悠”的意思啊。這里得到靈感,以后都用忽悠來翻譯Swing,呵呵。
5 Swing Thread Programming
為了把一些跟界面相關的代碼放回到Swing線程里去執行,Swing類庫中提供了方法,我懶得說了。
//把任務仍回給Swing執行,不阻塞本線程
invokeLater(Runnable GUITask)
//把任務仍回給Swing執行,阻塞本線程
invokeAndWait(Runnable GUITask)
當然,除了直接用這兩個方法處理長時間事件響應外,這幾天在網上還看到了另外兩個方法。
5.1 SwingWorker
提供了一個幫助類SwingWorker,使用方便。它把這些來來回回,麻麻煩煩的線程邏輯封裝起來。簡化使用Swing多線程的復雜度。
詳細了解,請點擊相關頁面。不好訪問,也可以本站下載pdf版本。
5.2 Event-Dispatch方法
使用觀察者模式重構系統,優化Swing多線程程序的設計。方法基本近似上文的“設計并行程序的好辦法”。它和SwingWork最大的區別就是,使用它是要重新設計系統的,會產生基于“觀察者模式”的角色類的。
詳細了解,請點擊相關頁面。不好訪問,也可以本站下載pdf版本。
6 結論
想借Swing Thread Programming探討一下多線程設計的一些初級問題,并推薦了一下“觀察者模式”。