全面解析Java8觀察者模式
觀察者(Observer)模式又名發(fā)布-訂閱(Publish/Subscribe)模式,是四人組(GoF,即 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides)在1994合著的《設計模式:可復用面向?qū)ο筌浖幕A》中提出的(詳見書中293-313頁)。盡管這種模式已經(jīng)有相當長的歷史,它仍然廣泛適用于各種場景,甚至成為了標準Java庫的一個組成部分。目前雖然已經(jīng)有大量關于觀察者模式的文章,但它們都專注于在 Java 中的實現(xiàn),卻忽視了開發(fā)者在Java中使用觀察者模式時遇到的各種問題。
本文的寫作初衷就是為了填補這一空白:本文主要介紹通過使用 Java8 架構實現(xiàn)觀察者模式,并在此基礎上進一步探討關于經(jīng)典模式的復雜問題,包括匿名內(nèi)部類、lambda 表達式、線程安全以及非平凡耗時長的觀察者實現(xiàn)。本文內(nèi)容雖然并不全面,很多這種模式所涉及的復雜問題,遠不是一篇文章就能說清的。但是讀完本文,讀者能了解什么是觀察者模式,它在Java中的通用性以及如何處理在 Java 中實現(xiàn)觀察者模式時的一些常見問題。
觀察者模式
根據(jù) GoF 提出的經(jīng)典定義,觀察者模式的主旨是:
定義對象間的一種一對多的依賴關系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新。
什么意思呢?很多軟件應用中,對象之間的狀態(tài)都是互相依賴的。例如,如果一個應用專注于數(shù)值數(shù)據(jù)加工,這個數(shù)據(jù)也許會通過圖形用戶界面(GUI)的表格或圖表來展現(xiàn)或者兩者同時使用,也就是說,當?shù)讓訑?shù)據(jù)更新時,相應的 GUI 組件也要更新。問題的關鍵在于如何做到底層數(shù)據(jù)更新時 GUI 組件也隨之更新,同時盡量減小 GUI 組件和底層數(shù)據(jù)的耦合度。
一種簡單且不可擴展的解決方案是給管理這些底層數(shù)據(jù)的對象該表格和圖像 GUI 組件的引用,使得對象可以在底層數(shù)據(jù)變化時能夠通知 GUI 組件。顯然,對于處理有更多 GUI 組件的復雜應用,這個簡單的解決方案很快顯示出其不足。例如,有20個 GUI 組件都依賴于底層數(shù)據(jù),那么管理底層數(shù)據(jù)的對象就需要維護指向這20個組件的引用。隨著依賴于相關數(shù)據(jù)的對象數(shù)量的增加,數(shù)據(jù)管理和對象之間的耦合度也變得難以控制。
另一個更好的解決方案是允許對象注冊獲取感興趣數(shù)據(jù)更新的權限,當數(shù)據(jù)變化時,數(shù)據(jù)管理器就會通知這些對象。通俗地說就是,讓感興趣的數(shù)據(jù)對象告訴管理器:“當數(shù)據(jù)變化時請通知我”。此外,這些對象不僅可以注冊獲取更新通知,也可以取消注冊,保證數(shù)據(jù)管理器在數(shù)據(jù)變化時不再通知該對象。在 GoF 的原始定義中,注冊獲取更新的對象叫作“觀察者”(observer),對應的數(shù)據(jù)管理器叫作“目標”(Subject),觀察者感興趣的數(shù)據(jù)叫作“目標狀態(tài)”,注冊過程叫“添加”(attach),撤銷觀察的過程叫“移除”(detach)。前文已經(jīng)提到觀察者模式又叫發(fā)布-訂閱模式,可以理解為客戶訂閱關于目標的觀察者,當目標狀態(tài)更新時,目標把這些更新發(fā)布給訂閱者(這種設計模式擴展為通用架構,稱為發(fā)布——訂閱架構)。這些概念可以用下面的類圖表示:
具體觀察者(ConcereteObserver)用來接收更新的狀態(tài)變化,同時將指向具體主題(ConcereteSubject)的引用傳遞給它的構造函數(shù)。這為具體觀察者提供了指向具體主題的引用,在狀態(tài)變化時可由此獲得更新。簡單來說,具體觀察者會被告知主題更新,同時用其構造函數(shù)中的引用來獲取具體主題的狀態(tài),最后將這些檢索狀態(tài)對象存儲在具體觀察者的觀察狀態(tài)(observerState)屬性下。這一過程如下面的序列圖所示:
經(jīng)典模式的專業(yè)化
盡管觀察者模式是通用的,但也有很多專業(yè)化的模式,最常見是以下兩種:
1、為State對象提供一個參數(shù),傳給觀察者調(diào)用的Update方法。在經(jīng)典模式下,當觀察者被通知Subject狀態(tài)發(fā)生變化后,會直接從Subject獲得其更新后狀態(tài)。這要求觀察者保存指向獲取狀態(tài)的對象引用。這樣就形成了一個循環(huán)引用,ConcreteSubject的引用指向其觀察者列表,ConcreteObserver的引用指向能獲得主題狀態(tài)的ConcreteSubject。除了獲得更新的狀態(tài),觀察者和其注冊監(jiān)聽的Subject間并沒有聯(lián)系,觀察者關心的是State對象,而非Subject本身。也就是說,很多情況下都將ConcreteObserver和ConcreteSubject強行聯(lián)系一起,相反,當ConcreteSubject調(diào)用Update函數(shù)時,將State對象傳遞給ConcreteObserver,二者就無需關聯(lián)。ConcreteObserver和State對象之間關聯(lián)減小了觀察者和State之間的依賴程度(關聯(lián)和依賴的更多區(qū)別請參見Martin Fowler's的文章)。
2、將Subject抽象類和ConcreteSubject合并到一個 singleSubject類中。多數(shù)情況下,Subject使用抽象類并不會提升程序的靈活性和可擴展性,因此,將這一抽象類和具體類合并簡化了設計。
這兩個專業(yè)化的模式組合后,其簡化類圖如下:
在這些專業(yè)化的模式中,靜態(tài)類結構大大簡化,類之間的相互作用也得以簡化。此時的序列圖如下:
專業(yè)化模式另一特點是刪除了 ConcreteObserver 的成員變量 observerState。有時候具體觀察者并不需要保存Subject的最新狀態(tài),而只需要監(jiān)測狀態(tài)更新時 Subject 的狀態(tài)。例如,如果觀察者將成員變量的值更新到標準輸出上,就可以刪除 observerState,這樣一來就刪除了ConcreteObserver和State類之間的關聯(lián)。
更常見的命名規(guī)則
經(jīng)典模式甚至是前文提到的專業(yè)化模式都用的是attach,detach和observer等術語,而Java實現(xiàn)中很多都是用的不同的詞典,包括register,unregister,listener等。值得一提的是State是listener需要監(jiān)測變化的所有對象的統(tǒng)稱,狀態(tài)對象的具體名稱需要看觀察者模式用到的場景。例如,在listener監(jiān)聽事件發(fā)生場景下的觀察者模式,已注冊的listener將會在事件發(fā)生時收到通知,此時的狀態(tài)對象就是event,也就是事件是否發(fā)生。
平時實際應用中目標的命名很少包含Subject。例如,創(chuàng)建一個關于動物園的應用,注冊多個監(jiān)聽器用于觀察Zoo類,并在新動物進入動物園時收到通知。該案例中的目標是Zoo類,為了和所給問題域保持術語一致,將不會用到Subject這樣的詞匯,也就是說Zoo類不會命名為ZooSubject。
監(jiān)聽器的命名一般都會跟著Listener后綴,例如前文提到的監(jiān)測新動物加入的監(jiān)聽器會命名為AnimalAddedListener。類似的,register,、unregister和notify等函數(shù)命名常會以其對應的監(jiān)聽器名作后綴,例如AnimalAddedListener的register、unregister、notify函數(shù)會被命名為registerAnimalAddedListener、 unregisterAnimalAddedListener和notifyAnimalAddedListeners,需要注意的是notify函數(shù)名的s,因為notify函數(shù)處理的是多個而非單一監(jiān)聽器。
這種命名方式會顯得冗長,而且通常一個subject會注冊多個類型的監(jiān)聽器,如前面提到的動物園的例子,Zoo內(nèi)除了注冊監(jiān)聽動物新增的監(jiān)聽器,還需注冊監(jiān)聽動物減少監(jiān)聽器,此時就會有兩種register函數(shù):(registerAnimalAddedListener和 registerAnimalRemovedListener,這種方式處理,監(jiān)聽器的類型作為一個限定符,表示其應觀察者的類型。另一解決方案是創(chuàng)建一個registerListener函數(shù)然后重載,但是方案一能更方便的知道哪個監(jiān)聽器正在監(jiān)聽,重載是比較小眾的做法。
另一慣用語法是用on前綴而不是update,例如update函數(shù)命名為onAnimalAdded而不是updateAnimalAdded。這種情況在監(jiān)聽器獲得一個序列的通知時更常見,如向list中新增一個動物,但很少用于更新一個單獨的數(shù)據(jù),比如動物的名字。
接下來本文將使用Java的符號規(guī)則,雖然符號規(guī)則不會改變系統(tǒng)的真實設計和實現(xiàn),但是使用其他開發(fā)者都熟悉的術語是很重要的開發(fā)準則,因此要熟悉上文描述的Java中的觀察者模式符號規(guī)則。下文將在Java8環(huán)境下用一個簡單例子來闡述上述概念。
一個簡單的實例
還是前面提到的動物園的例子,使用Java8的API接口實現(xiàn)一個簡單的系統(tǒng),說明觀察者模式的基本原理。問題描述為:
創(chuàng)建一個系統(tǒng)zoo,允許用戶監(jiān)聽和撤銷監(jiān)聽添加新對象animal的狀態(tài),另外再創(chuàng)建一個具體監(jiān)聽器,負責輸出新增動物的name。
根據(jù)前面對觀察者模式的學習知道實現(xiàn)這樣的應用需要創(chuàng)建4個類,具體是:
- Zoo類:即模式中的主題,負責存儲動物園中的所有動物,并在新動物加入時通知所有已注冊的監(jiān)聽器。
- Animal類:代表動物對象。
- AnimalAddedListener類:即觀察者接口。
- PrintNameAnimalAddedListener:具體的觀察者類,負責輸出新增動物的name。
首先我們創(chuàng)建一個Animal類,它是一個包含name成員變量、構造函數(shù)、getter和setter方法的簡單Java對象,代碼如下:
public class Animal { private String name; public Animal (String name) { this.name = name; } public String getName () { return this.name; } public void setName (String name) { this.name = name; } }
用這個類代表動物對象,接下來就可以創(chuàng)建AnimalAddedListener接口了:
public interface AnimalAddedListener { public void onAnimalAdded (Animal animal); }
前面兩個類很簡單,就不再詳細介紹,接下來創(chuàng)建Zoo類:
public class Zoo { private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public void registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } protected void notifyAnimalAddedListeners (Animal animal) { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } }
這個類比前面兩個都復雜,其包含兩個list,一個用來存儲動物園中所有動物,另一個用來存儲所有的監(jiān)聽器,鑒于animals和listener集合存儲的對象都很簡單,本文選擇了ArrayList來存儲。存儲監(jiān)聽器的具體數(shù)據(jù)結構要視問題而定,比如對于這里的動物園問題,如果監(jiān)聽器有優(yōu)先級,那就應該選擇其他的數(shù)據(jù)結構,或者重寫監(jiān)聽器的register算法。
注冊和移除的實現(xiàn)都是簡單的委托方式:各個監(jiān)聽器作為參數(shù)從監(jiān)聽者的監(jiān)聽列表增加或者移除。notify函數(shù)的實現(xiàn)與觀察者模式的標準格式稍微偏離,它包括輸入?yún)?shù):新增加的animal,這樣一來notify函數(shù)就可以把新增加的animal引用傳遞給監(jiān)聽器了。用streams API的forEach函數(shù)遍歷監(jiān)聽器,對每個監(jiān)聽器執(zhí)行theonAnimalAdded函數(shù)。
在addAnimal函數(shù)中,新增的animal對象和監(jiān)聽器各自添加到對應list。如果不考慮通知過程的復雜性,這一邏輯應包含在方便調(diào)用的方法中,只需要傳入指向新增animal對象的引用即可,這就是通知監(jiān)聽器的邏輯實現(xiàn)封裝在notifyAnimalAddedListeners函數(shù)中的原因,這一點在addAnimal的實現(xiàn)中也提到過。
除了notify函數(shù)的邏輯問題,需要強調(diào)一下對notify函數(shù)可見性的爭議問題。在經(jīng)典的觀察者模型中,如GoF在設計模式一書中第301頁所說,notify函數(shù)是public型的,然而盡管在經(jīng)典模式中用到,這并不意味著必須是public的。選擇可見性應該基于應用,例如本文的動物園的例子,notify函數(shù)是protected類型,并不要求每個對象都可以發(fā)起一個注冊觀察者的通知,只需保證對象能從父類繼承該功能即可。當然,也并非完全如此,需要弄清楚哪些類可以激活notify函數(shù),然后再由此確定函數(shù)的可見性。
接下來需要實現(xiàn)PrintNameAnimalAddedListener類,這個類用System.out.println方法將新增動物的name輸出,具體代碼如下:
public class PrintNameAnimalAddedListener implements AnimalAddedListener { @Override public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal System.out.println("Added a new animal with name '" + animal.getName() + "'"); } }
最后要實現(xiàn)驅(qū)動應用的主函數(shù):
public class Main { public static void main (String[] args) { // Create the zoo to store animals Zoo zoo = new Zoo(); // Register a listener to be notified when an animal is added zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener()); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); } }
主函數(shù)只是簡單的創(chuàng)建了一個zoo對象,注冊了一個輸出動物name的監(jiān)聽器,并新建了一個animal對象以觸發(fā)已注冊的監(jiān)聽器,最后的輸出為:
Added a new animal with name 'Tiger'
新增監(jiān)聽器
當監(jiān)聽器重新建立并將其添加到Subject時,觀察者模式的優(yōu)勢就充分顯示出來。例如,想添加一個計算動物園中動物總數(shù)的監(jiān)聽器,只需要新建一個具體的監(jiān)聽器類并注冊到Zoo類即可,而無需對zoo類做任何修改。添加計數(shù)監(jiān)聽器CountingAnimalAddedListener代碼如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); } }
修改后的main函數(shù)如下:
public class Main { public static void main (String[] args) { // Create the zoo to store animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener(new PrintNameAnimalAddedListener()); zoo.registerAnimalAddedListener(new CountingAnimalAddedListener()); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); zoo.addAnimal(new Animal("Lion")); zoo.addAnimal(new Animal("Bear")); } }
輸出結果為:
Added a new animal with name 'Tiger' Total animals added: 1 Added a new animal with name 'Lion' Total animals added: 2 Added a new animal with name 'Bear' Total animals added: 3
使用者可在僅修改監(jiān)聽器注冊代碼的情況下,創(chuàng)建任意監(jiān)聽器。具有此可擴展性主要是因為Subject和觀察者接口關聯(lián),而不是直接和ConcreteObserver關聯(lián)。只要接口不被修改,調(diào)用接口的Subject就無需修改。
匿名內(nèi)部類,Lambda函數(shù)和監(jiān)聽器注冊
Java8的一大改進是增加了功能特性,如增加了lambda函數(shù)。在引進lambda函數(shù)之前,Java通過匿名內(nèi)部類提供了類似的功能,這些類在很多已有的應用中仍在使用。在觀察者模式下,隨時可以創(chuàng)建新的監(jiān)聽器而無需創(chuàng)建具體觀察者類,例如,PrintNameAnimalAddedListener類可以在main函數(shù)中用匿名內(nèi)部類實現(xiàn),具體實現(xiàn)代碼如下:
public class Main { public static void main (String[] args) { // Create the zoo to store animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener(new AnimalAddedListener() { @Override public void updateAnimalAdded (Animal animal) { // Print the name of the newly added animal System.out.println("Added a new animal with name '" + animal.getName() + "'"); } }); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); } }
類似的,lambda函數(shù)也可以用以完成此類任務:
public class Main { public static void main (String[] args) { // Create the zoo to store animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added zoo.registerAnimalAddedListener( (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'") ); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); } }
需要注意的是lambda函數(shù)僅適用于監(jiān)聽器接口只有一個函數(shù)的情況,這個要求雖然看起來嚴格,但實際上很多監(jiān)聽器都是單一函數(shù)的,如示例中的AnimalAddedListener。如果接口有多個函數(shù),可以選擇使用匿名內(nèi)部類。
隱式注冊創(chuàng)建的監(jiān)聽器存在此類問題:由于對象是在注冊調(diào)用的范圍內(nèi)創(chuàng)建的,所以不可能將引用存儲一個到具體監(jiān)聽器。這意味著,通過lambda函數(shù)或者匿名內(nèi)部類注冊的監(jiān)聽器不可以撤銷注冊,因為撤銷函數(shù)需要傳入已經(jīng)注冊監(jiān)聽器的引用。解決這個問題的一個簡單方法是在registerAnimalAddedListener函數(shù)中返回注冊監(jiān)聽器的引用。如此一來,就可以撤銷注冊用lambda函數(shù)或匿名內(nèi)部類創(chuàng)建的監(jiān)聽器,改進后的方法代碼如下:
public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; }
重新設計的函數(shù)交互的客戶端代碼如下:
public class Main { public static void main (String[] args) { // Create the zoo to store animals Zoo zoo = new Zoo(); // Register listeners to be notified when an animal is added AnimalAddedListener listener = zoo.registerAnimalAddedListener( (animal) -> System.out.println("Added a new animal with name '" + animal.getName() + "'") ); // Add an animal notify the registered listeners zoo.addAnimal(new Animal("Tiger")); // Unregister the listener zoo.unregisterAnimalAddedListener(listener); // Add another animal, which will not print the name, since the listener // has been previously unregistered zoo.addAnimal(new Animal("Lion")); } }
此時的結果輸出只有Added a new animal with name ‘Tiger',因為在第二個animal加入之前監(jiān)聽器已經(jīng)撤銷了:
Added a new animal with name 'Tiger'
如果采用更復雜的解決方案,register函數(shù)也可以返回receipt類,以便unregister監(jiān)聽器調(diào)用,例如:
public class AnimalAddedListenerReceipt { private final AnimalAddedListener listener; public AnimalAddedListenerReceipt (AnimalAddedListener listener) { this.listener = listener; } public final AnimalAddedListener getListener () { return this.listener; } }
receipt會作為注冊函數(shù)的返回值,以及撤銷注冊函數(shù)輸入?yún)?shù),此時的zoo實現(xiàn)如下所示:
public class ZooUsingReceipt { // ...Existing attributes and constructor... public AnimalAddedListenerReceipt registerAnimalAddedListener (AnimalAddedListener listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return new AnimalAddedListenerReceipt(listener); } public void unregisterAnimalAddedListener (AnimalAddedListenerReceipt receipt) { // Remove the listener from the list of the registered listeners this.listeners.remove(receipt.getListener()); } // ...Existing notification method... }
上面描述的接收實現(xiàn)機制允許保存信息供監(jiān)聽器撤銷時調(diào)用的,也就是說如果撤銷注冊算法依賴于Subject注冊監(jiān)聽器時的狀態(tài),則此狀態(tài)將被保存,如果撤銷注冊只需要指向之前注冊監(jiān)聽器的引用,這樣的話接收技術則顯得麻煩,不推薦使用。
除了特別復雜的具體監(jiān)聽器,最常見的注冊監(jiān)聽器的方法是通過lambda函數(shù)或通過匿名內(nèi)部類注冊。當然,也有例外,那就是包含subject實現(xiàn)觀察者接口的類和注冊一個包含調(diào)用該引用目標的監(jiān)聽器。如下面代碼所示的案例:
public class ZooContainer implements AnimalAddedListener { private Zoo zoo = new Zoo(); public ZooContainer () { // Register this object as a listener this.zoo.registerAnimalAddedListener(this); } public Zoo getZoo () { return this.zoo; } @Override public void updateAnimalAdded (Animal animal) { System.out.println("Added animal with name '" + animal.getName() + "'"); } public static void main (String[] args) { // Create the zoo container ZooContainer zooContainer = new ZooContainer(); // Add an animal notify the innerally notified listener zooContainer.getZoo().addAnimal(new Animal("Tiger")); } }
這種方法只適用于簡單情況而且代碼看起來不夠?qū)I(yè),盡管如此,它還是深受現(xiàn)代Java開發(fā)人員的喜愛,因此了解這個例子的工作原理很有必要。因為ZooContainer實現(xiàn)了AnimalAddedListener接口,那么ZooContainer的實例(或者說對象)就可以注冊為AnimalAddedListener。ZooContainer類中,該引用代表當前對象即ZooContainer的一個實例,所以可以被用作AnimalAddedListener。
通常,不是要求所有的container類都實現(xiàn)此類功能,而且實現(xiàn)監(jiān)聽器接口的container類只能調(diào)用Subject的注冊函數(shù),只是簡單把該引用作為監(jiān)聽器的對象傳給register函數(shù)。在接下來的章節(jié)中,將介紹多線程環(huán)境的常見問題和解決方案。
線程安全的實現(xiàn)
前面章節(jié)介紹了在現(xiàn)代Java環(huán)境下的實現(xiàn)觀察者模式,雖然簡單但很完整,但這一實現(xiàn)忽略了一個關鍵性問題:線程安全。大多數(shù)開放的Java應用都是多線程的,而且觀察者模式也多用于多線程或異步系統(tǒng)。例如,如果外部服務更新其數(shù)據(jù)庫,那么應用也會異步地收到消息,然后用觀察者模式通知內(nèi)部組件更新,而不是內(nèi)部組件直接注冊監(jiān)聽外部服務。
觀察者模式的線程安全主要集中在模式的主體上,因為修改注冊監(jiān)聽器集合時很可能發(fā)生線程沖突,比如,一個線程試圖添加一個新的監(jiān)聽器,而另一線程又試圖添加一個新的animal對象,這將觸發(fā)對所有注冊監(jiān)聽器的通知。鑒于先后順序,在已注冊的監(jiān)聽器收到新增動物的通知前,第一個線程可能已經(jīng)完成也可能尚未完成新監(jiān)聽器的注冊。這是一個經(jīng)典的線程資源競爭案例,正是這一現(xiàn)象告訴開發(fā)者們需要一個機制來保證線程安全。
這一問題的最簡單的解決方案是:所有訪問或修改注冊監(jiān)聽器list的操作都須遵循Java的同步機制,比如:
public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ } public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ } public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }
這樣一來,同一時刻只有一個線程可以修改或訪問已注冊的監(jiān)聽器列表,可以成功地避免資源競爭問題,但是新問題又出現(xiàn)了,這樣的約束太過嚴格(synchronized關鍵字和Java并發(fā)模型的更多信息,請參閱官方網(wǎng)頁)。通過方法同步,可以時刻觀測對監(jiān)聽器list的并發(fā)訪問,注冊和撤銷監(jiān)聽器對監(jiān)聽器list而言是寫操作,而通知監(jiān)聽器訪問監(jiān)聽器list是只讀操作。由于通過通知訪問是讀操作,因此是可以多個通知操作同時進行的。
因此,只要沒有監(jiān)聽器注冊或撤銷注冊,任意多的并發(fā)通知都可以同時執(zhí)行,而不會引發(fā)對注冊的監(jiān)聽器列表的資源爭奪。當然,其他情況下的資源爭奪現(xiàn)象存在已久,為了解決這一問題,設計了ReadWriteLock用以分開管理讀寫操作的資源鎖定。Zoo類的線程安全ThreadSafeZoo實現(xiàn)代碼如下:
public class ThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); } } }
通過這樣部署,Subject的實現(xiàn)能確保線程安全并且多個線程可以同時發(fā)布通知。但盡管如此,依舊存在兩個不容忽略的資源競爭問題:
對每個監(jiān)聽器的并發(fā)訪問。多個線程可以同時通知監(jiān)聽器要新增動物了,這意味著一個監(jiān)聽器可能會同時被多個線程同時調(diào)用。
對animal list的并發(fā)訪問。多個線程可能會同時向animal list添加對象,如果通知的先后順序存在影響,那就可能導致資源競爭,這就需要一個并發(fā)操作處理機制來避免這一問題。如果注冊的監(jiān)聽器列表在收到通知添加animal2后,又收到通知添加animal1,此時就會產(chǎn)生資源競爭。但是如果animal1和animal2的添加由不同的線程執(zhí)行,也是有可能在animal2前完成對animal1添加操作,具體來說就是線程1在通知監(jiān)聽器前添加animal1并鎖定模塊,線程2添加animal2并通知監(jiān)聽器,然后線程1通知監(jiān)聽器animal1已經(jīng)添加。雖然在不考慮先后順序時,可以忽略資源競爭,但問題是真實存在的。
對監(jiān)聽器的并發(fā)訪問
并發(fā)訪問監(jiān)聽器可以通過保證監(jiān)聽器的線程安全來實現(xiàn)。秉承著類的“責任自負”精神,監(jiān)聽器有“義務”確保自身的線程安全。例如,對于前面計數(shù)的監(jiān)聽器,多線程的遞增或遞減動物數(shù)量可能導致線程安全問題,要避免這一問題,動物數(shù)的計算必須是原子操作(原子變量或方法同步),具體解決代碼如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); } }
方法同步解決方案代碼如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); } }
要強調(diào)的是監(jiān)聽器應該保證自身的線程安全,subject需要理解監(jiān)聽器的內(nèi)部邏輯,而不是簡單確保對監(jiān)聽器的訪問和修改的線程安全。否則,如果多個subject共用同一個監(jiān)聽器,那每個subject類都要重寫一遍線程安全的代碼,顯然這樣的代碼不夠簡潔,因此需要在監(jiān)聽器類內(nèi)實現(xiàn)線程安全。
監(jiān)聽器的有序通知
當要求監(jiān)聽器有序執(zhí)行時,讀寫鎖就不能滿足需求了,而需要引入一個新的機制,可以保證notify函數(shù)的調(diào)用順序和animal添加到zoo的順序一致。有人嘗試過用方法同步來實現(xiàn),然而根據(jù)Oracle文檔中的方法同步介紹,可知方法同步并不提供操作執(zhí)行的順序管理。它只是保證原子操作,也就是說操作不會被打斷,并不能保證先來先執(zhí)行(FIFO)的線程順序。ReentrantReadWriteLock可以實現(xiàn)這樣的執(zhí)行順序,代碼如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); } } }
這樣的實現(xiàn)方式,register, unregister和notify函數(shù)將按照先進先出(FIFO)的順序獲得讀寫鎖權限。例如,線程1注冊一個監(jiān)聽器,線程2在開始執(zhí)行注冊操作后試圖通知已注冊的監(jiān)聽器,線程3在線程2等待只讀鎖的時候也試圖通知已注冊的監(jiān)聽器,采用fair-ordering方式,線程1先完成注冊操作,然后線程2可以通知監(jiān)聽器,最后線程3通知監(jiān)聽器。這樣保證了action的執(zhí)行順序和開始順序一致。
如果采用方法同步,雖然線程2先排隊等待占用資源,線程3仍可能比線程2先獲得資源鎖,而且不能保證線程2比線程3先通知監(jiān)聽器。問題的關鍵所在:fair-ordering方式可以保證線程按照申請資源的順序執(zhí)行。讀寫鎖的順序機制很復雜,應參照ReentrantReadWriteLock的官方文檔以確保鎖的邏輯足夠解決問題。
截止目前實現(xiàn)了線程安全,在接下來的章節(jié)中將介紹提取主題的邏輯并將其mixin類封裝為可重復代碼單元的方式優(yōu)缺點。
主題邏輯封裝到Mixin類
把上述的觀察者模式設計實現(xiàn)封裝到目標的mixin類中很具吸引力。通常來說,觀察者模式中的觀察者包含已注冊的監(jiān)聽器的集合;負責注冊新的監(jiān)聽器的register函數(shù);負責撤銷注冊的unregister函數(shù)和負責通知監(jiān)聽器的notify函數(shù)。對于上述的動物園的例子,zoo類除動物列表是問題所需外,其他所有操作都是為了實現(xiàn)主題的邏輯。
Mixin類的案例如下所示,需要說明的是為使代碼更為簡潔,此處去掉關于線程安全的代碼:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); } }
正因為沒有提供正在注冊的監(jiān)聽器類型的接口信息,不能直接通知某個特定的監(jiān)聽器,所以正需要保證通知功能的通用性,允許客戶端添加一些功能,如接受泛型參數(shù)類型的參數(shù)匹配,以適用于每個監(jiān)聽器,具體實現(xiàn)代碼如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); } }
Mixin類技術的最大優(yōu)勢是把觀察者模式的Subject封裝到一個可重復調(diào)用的類中,而不是在每個subject類中都重復寫這些邏輯。此外,這一方法使得zoo類的實現(xiàn)更為簡潔,只需要存儲動物信息,而不用再考慮如何存儲和通知監(jiān)聽器。
然而,使用mixin類并非只有優(yōu)點。比如,如果要存儲多個類型的監(jiān)聽器怎么辦?例如,還需要存儲監(jiān)聽器類型AnimalRemovedListener。mixin類是抽象類,Java中不能同時繼承多個抽象類,而且mixin類不能改用接口實現(xiàn),這是因為接口不包含state,而觀察者模式中state需要用來保存已經(jīng)注冊的監(jiān)聽器列表。
其中的一個解決方案是創(chuàng)建一個動物增加和減少時都會通知的監(jiān)聽器類型ZooListener,代碼如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal); }
這樣就可以使用該接口實現(xiàn)利用一個監(jiān)聽器類型對zoo狀態(tài)各種變化的監(jiān)聽了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); } }
將多個監(jiān)聽器類型合并到一個監(jiān)聽器接口中確實解決了上面提到的問題,但仍舊存在不足之處,接下來的章節(jié)會詳細討論。
Multi-Method監(jiān)聽器和適配器
在上述方法,監(jiān)聽器的接口中實現(xiàn)的包含太多函數(shù),接口就過于冗長,例如,Swing MouseListener就包含5個必要的函數(shù)。盡管可能只會用到其中一個,但是只要用到鼠標點擊事件就必須要添加這5個函數(shù),更多可能是用空函數(shù)體來實現(xiàn)剩下的函數(shù),這無疑會給代碼帶來不必要的混亂。
其中一種解決方案是創(chuàng)建適配器(概念來自GoF提出的適配器模式),適配器中以抽象函數(shù)的形式實現(xiàn)監(jiān)聽器接口的操作,供具體監(jiān)聽器類繼承。這樣一來,具體監(jiān)聽器類就可以選擇其需要的函數(shù),對adapter不需要的函數(shù)采用默認操作即可。例如上面例子中的ZooListener類,創(chuàng)建ZooAdapter(Adapter的命名規(guī)則與監(jiān)聽器一致,只需要把類名中的Listener改為Adapter即可),代碼如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {} }
乍一看,這個適配器類微不足道,然而它所帶來的便利卻是不可小覷的。比如對于下面的具體類,只需選擇對其實現(xiàn)有用的函數(shù)即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); } }
有兩種替代方案同樣可以實現(xiàn)適配器類的功能:一是使用默認函數(shù);二是把監(jiān)聽器接口和適配器類合并到一個具體類中。默認函數(shù)是Java8新提出的,在接口中允許開發(fā)者提供默認(防御)的實現(xiàn)方法。
Java庫的這一更新主要是方便開發(fā)者在不改變老版本代碼的情況下,實現(xiàn)程序擴展,因此應該慎用這個方法。部分開發(fā)者多次使用后,會感覺這樣寫的代碼不夠?qū)I(yè),而又有開發(fā)者認為這是Java8的特色,不管怎樣,需要明白這個技術提出的初衷是什么,再結合具體問題決定是否要用。使用默認函數(shù)實現(xiàn)的ZooListener接口代碼如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {} }
通過使用默認函數(shù),實現(xiàn)該接口的具體類,無需在接口中實現(xiàn)全部函數(shù),而是選擇性實現(xiàn)所需函數(shù)。雖然這是接口膨脹問題一個較為簡潔的解決方案,開發(fā)者在使用時還應多加注意。
第二種方案是簡化觀察者模式,省略了監(jiān)聽器接口,而是用具體類實現(xiàn)監(jiān)聽器的功能。比如ZooListener接口就變成了下面這樣:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {} }
這一方案簡化了觀察者模式的層次結構,但它并非適用于所有情況,因為如果把監(jiān)聽器接口合并到具體類中,具體監(jiān)聽器就不可以實現(xiàn)多個監(jiān)聽接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口寫在同一個具體類中,那么單獨一個具體監(jiān)聽器就不可以同時實現(xiàn)這兩個接口了。此外,監(jiān)聽器接口的意圖比具體類更顯而易見,很顯然前者就是為其他類提供接口,但后者就并非那么明顯了。
如果沒有合適的文檔說明,開發(fā)者并不會知道已經(jīng)有一個類扮演著接口的角色,實現(xiàn)了其對應的所有函數(shù)。此外,類名不包含adapter,因為類并不適配于某一個接口,因此類名并沒有特別暗示此意圖。綜上所述,特定問題需要選擇特定的方法,并沒有哪個方法是萬能的。
在開始下一章前,需要特別提一下,適配器在觀察模式中很常見,尤其是在老版本的Java代碼中。Swing API正是以適配器為基礎實現(xiàn)的,正如很多老應用在Java5和Java6中的觀察者模式中所使用的那樣。zoo案例中的監(jiān)聽器或許并不需要適配器,但需要了解適配器提出的目的以及其應用,因為我們可以在現(xiàn)有的代碼中對其進行使用。下面的章節(jié),將會介紹時間復雜的監(jiān)聽器,該類監(jiān)聽器可能會執(zhí)行耗時的運算或進行異步調(diào)用,不能立即給出返回值。
Complex & Blocking監(jiān)聽器
關于觀察者模式的一個假設是:執(zhí)行一個函數(shù)時,一系列監(jiān)聽器會被調(diào)用,但假定這一過程對調(diào)用者而言是完全透明的。例如,客戶端代碼在Zoo中添加animal時,在返回添加成功之前,并不知道會調(diào)用一系列監(jiān)聽器。如果監(jiān)聽器的執(zhí)行需要時間較長(其時間受監(jiān)聽器的數(shù)量、每個監(jiān)聽器執(zhí)行時間影響),那么客戶端代碼將會感知這一簡單增加動物操作的時間副作用。
本文不能面面俱到的討論這個話題,下面幾條是開發(fā)者調(diào)用復雜的監(jiān)聽器時應該注意的事項:
監(jiān)聽器啟動新線程。新線程啟動后,在新線程中執(zhí)行監(jiān)聽器邏輯的同時,返回監(jiān)聽器函數(shù)的處理結果,并運行其他監(jiān)聽器執(zhí)行。
Subject啟動新線程。與傳統(tǒng)的線性迭代已注冊的監(jiān)聽器列表不同,Subject的notify函數(shù)重啟一個新的線程,然后在新線程中迭代監(jiān)聽器列表。這樣使得notify函數(shù)在執(zhí)行其他監(jiān)聽器操作的同時可以輸出其返回值。需要注意的是需要一個線程安全機制來確保監(jiān)聽器列表不會進行并發(fā)修改。
隊列化監(jiān)聽器調(diào)用并采用一組線程執(zhí)行監(jiān)聽功能。將監(jiān)聽器操作封裝在一些函數(shù)中并隊列化這些函數(shù),而非簡單的迭代調(diào)用監(jiān)聽器列表。這些監(jiān)聽器存儲到隊列中后,線程就可以從隊列中彈出單個元素并執(zhí)行其監(jiān)聽邏輯。這類似于生產(chǎn)者-消費者問題,notify過程產(chǎn)生可執(zhí)行函數(shù)隊列,然后線程依次從隊列中取出并執(zhí)行這些函數(shù),函數(shù)需要存儲被創(chuàng)建的時間而非執(zhí)行的時間供監(jiān)聽器函數(shù)調(diào)用。例如,監(jiān)聽器被調(diào)用時創(chuàng)建的函數(shù),那么該函數(shù)就需要存儲該時間點,這一功能類似于Java中的如下操作:
public class
如何使用Java8 實現(xiàn)觀察者模式?相信通過這篇文章大家都有了大概的了解了吧!
相關文章
阿里Sentinel支持Spring Cloud Gateway的實現(xiàn)
這篇文章主要介紹了阿里Sentinel支持Spring Cloud Gateway的實現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04jackson 實體轉json 為NULL或者為空不參加序列化(實例講解)
下面小編就為大家?guī)硪黄猨ackson 實體轉json 為NULL或者為空不參加序列化(實例講解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10springboot如何讀取自定義properties并注入到bean中
這篇文章主要介紹了springboot讀取自定義properties并注入到bean中,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11