日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

如何使用 Java8 实现观察者模式?(下)

發布時間:2025/6/15 java 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何使用 Java8 实现观察者模式?(下) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

2019獨角獸企業重金招聘Python工程師標準>>>

【編者按】本文作者是 BAE 系統公司的軟件工程師 Justin Albano。在本篇文章中,作者通過在 Java8 環境下實現觀察者模式的實例,進一步介紹了什么是觀察者模式、專業化及其命名規則,供大家參考學習。本文系國內 ITOM 管理平臺 OneAPM 工程師編譯整理。 ##線程安全的實現

前面章節介紹了在現代Java環境下的實現觀察者模式,雖然簡單但很完整,但這一實現忽略了一個關鍵性問題:線程安全。大多數開放的Java應用都是多線程的,而且觀察者模式也多用于多線程或異步系統。例如,如果外部服務更新其數據庫,那么應用也會異步地收到消息,然后用觀察者模式通知內部組件更新,而不是內部組件直接注冊監聽外部服務。

觀察者模式的線程安全主要集中在模式的主體上,因為修改注冊監聽器集合時很可能發生線程沖突,比如,一個線程試圖添加一個新的監聽器,而另一線程又試圖添加一個新的animal對象,這將觸發對所有注冊監聽器的通知。鑒于先后順序,在已注冊的監聽器收到新增動物的通知前,第一個線程可能已經完成也可能尚未完成新監聽器的注冊。這是一個經典的線程資源競爭案例,正是這一現象告訴開發者們需要一個機制來保證線程安全。

這一問題的最簡單的解決方案是:所有訪問或修改注冊監聽器list的操作都須遵循Java的同步機制,比如:

public synchronized AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { /*...*/ } public synchronized void unregisterAnimalAddedListener (AnimalAddedListener listener) { /*...*/ } public synchronized void notifyAnimalAddedListeners (Animal animal) { /*...*/ }

這樣一來,同一時刻只有一個線程可以修改或訪問已注冊的監聽器列表,可以成功地避免資源競爭問題,但是新問題又出現了,這樣的約束太過嚴格(synchronized關鍵字和Java并發模型的更多信息,請參閱官方網頁)。通過方法同步,可以時刻觀測對監聽器list的并發訪問,注冊和撤銷監聽器對監聽器list而言是寫操作,而通知監聽器訪問監聽器list是只讀操作。由于通過通知訪問是讀操作,因此是可以多個通知操作同時進行的。

因此,只要沒有監聽器注冊或撤銷注冊,任意多的并發通知都可以同時執行,而不會引發對注冊的監聽器列表的資源爭奪。當然,其他情況下的資源爭奪現象存在已久,為了解決這一問題,設計了ReadWriteLock用以分開管理讀寫操作的資源鎖定。Zoo類的線程安全ThreadSafeZoo實現代碼如下:

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 animalsthis.animals.add(animal);// Notify the list of registered listenersthis.notifyAnimalAddedListeners(animal);}public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {// Lock the list of listeners for writingthis.writeLock.lock();try {// Add the listener to the list of registered listenersthis.listeners.add(listener);}finally {// Unlock the writer lockthis.writeLock.unlock();}return listener;}public void unregisterAnimalAddedListener (AnimalAddedListener listener) {// Lock the list of listeners for writingthis.writeLock.lock();try {// Remove the listener from the list of the registered listenersthis.listeners.remove(listener);}finally {// Unlock the writer lockthis.writeLock.unlock();}}public void notifyAnimalAddedListeners (Animal animal) {// Lock the list of listeners for readingthis.readLock.lock();try {// Notify each of the listeners in the list of registered listenersthis.listeners.forEach(listener -> listener.updateAnimalAdded(animal));}finally {// Unlock the reader lockthis.readLock.unlock();}} }

通過這樣部署,Subject的實現能確保線程安全并且多個線程可以同時發布通知。但盡管如此,依舊存在兩個不容忽略的資源競爭問題:

  • 對每個監聽器的并發訪問。多個線程可以同時通知監聽器要新增動物了,這意味著一個監聽器可能會同時被多個線程同時調用。

  • 對animal list的并發訪問。多個線程可能會同時向animal list添加對象,如果通知的先后順序存在影響,那就可能導致資源競爭,這就需要一個并發操作處理機制來避免這一問題。如果注冊的監聽器列表在收到通知添加animal2后,又收到通知添加animal1,此時就會產生資源競爭。但是如果animal1和animal2的添加由不同的線程執行,也是有可能在animal2前完成對animal1添加操作,具體來說就是線程1在通知監聽器前添加animal1并鎖定模塊,線程2添加animal2并通知監聽器,然后線程1通知監聽器animal1已經添加。雖然在不考慮先后順序時,可以忽略資源競爭,但問題是真實存在的。

  • ##對監聽器的并發訪問

    并發訪問監聽器可以通過保證監聽器的線程安全來實現。秉承著類的“責任自負”精神,監聽器有“義務”確保自身的線程安全。例如,對于前面計數的監聽器,多線程的遞增或遞減動物數量可能導致線程安全問題,要避免這一問題,動物數的計算必須是原子操作(原子變量或方法同步),具體解決代碼如下:

    public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener {private static AtomicLong animalsAddedCount = new AtomicLong(0);@Overridepublic void updateAnimalAdded (Animal animal) {// Increment the number of animalsanimalsAddedCount.incrementAndGet();// Print the number of animalsSystem.out.println("Total animals added: " + animalsAddedCount);} }

    方法同步解決方案代碼如下:

    public class CountingAnimalAddedListener implements AnimalAddedListener {private static int animalsAddedCount = 0;@Overridepublic synchronized void updateAnimalAdded (Animal animal) {// Increment the number of animalsanimalsAddedCount++;// Print the number of animalsSystem.out.println("Total animals added: " + animalsAddedCount);} }

    要強調的是監聽器應該保證自身的線程安全,subject需要理解監聽器的內部邏輯,而不是簡單確保對監聽器的訪問和修改的線程安全。否則,如果多個subject共用同一個監聽器,那每個subject類都要重寫一遍線程安全的代碼,顯然這樣的代碼不夠簡潔,因此需要在監聽器類內實現線程安全。

    ##監聽器的有序通知

    當要求監聽器有序執行時,讀寫鎖就不能滿足需求了,而需要引入一個新的機制,可以保證notify函數的調用順序和animal添加到zoo的順序一致。有人嘗試過用方法同步來實現,然而根據Oracle文檔中的方法同步介紹,可知方法同步并不提供操作執行的順序管理。它只是保證原子操作,也就是說操作不會被打斷,并不能保證先來先執行(FIFO)的線程順序。ReentrantReadWriteLock可以實現這樣的執行順序,代碼如下:

    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 animalsthis.animals.add(animal);// Notify the list of registered listenersthis.notifyAnimalAddedListeners(animal);}public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) {// Lock the list of listeners for writingthis.writeLock.lock();try {// Add the listener to the list of registered listenersthis.listeners.add(listener);}finally {// Unlock the writer lockthis.writeLock.unlock();}return listener;}public void unregisterAnimalAddedListener (AnimalAddedListener listener) {// Lock the list of listeners for writingthis.writeLock.lock();try {// Remove the listener from the list of the registered listenersthis.listeners.remove(listener);}finally {// Unlock the writer lockthis.writeLock.unlock();}}public void notifyAnimalAddedListeners (Animal animal) {// Lock the list of listeners for readingthis.readLock.lock();try {// Notify each of the listeners in the list of registered listenersthis.listeners.forEach(listener -> listener.updateAnimalAdded(animal));}finally {// Unlock the reader lockthis.readLock.unlock();}} }

    這樣的實現方式,register, unregister和notify函數將按照先進先出(FIFO)的順序獲得讀寫鎖權限。例如,線程1注冊一個監聽器,線程2在開始執行注冊操作后試圖通知已注冊的監聽器,線程3在線程2等待只讀鎖的時候也試圖通知已注冊的監聽器,采用fair-ordering方式,線程1先完成注冊操作,然后線程2可以通知監聽器,最后線程3通知監聽器。這樣保證了action的執行順序和開始順序一致。

    如果采用方法同步,雖然線程2先排隊等待占用資源,線程3仍可能比線程2先獲得資源鎖,而且不能保證線程2比線程3先通知監聽器。問題的關鍵所在:fair-ordering方式可以保證線程按照申請資源的順序執行。讀寫鎖的順序機制很復雜,應參照ReentrantReadWriteLock的官方文檔以確保鎖的邏輯足夠解決問題。

    截止目前實現了線程安全,在接下來的章節中將介紹提取主題的邏輯并將其mixin類封裝為可重復代碼單元的方式優缺點。

    ##主題邏輯封裝到Mixin類

    把上述的觀察者模式設計實現封裝到目標的mixin類中很具吸引力。通常來說,觀察者模式中的觀察者包含已注冊的監聽器的集合;負責注冊新的監聽器的register函數;負責撤銷注冊的unregister函數和負責通知監聽器的notify函數。對于上述的動物園的例子,zoo類除動物列表是問題所需外,其他所有操作都是為了實現主題的邏輯。

    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 listenersthis.listeners.add(listener);return listener;}public void unregisterAnimalAddedListener (ListenerType listener) {// Remove the listener from the list of the registered listenersthis.listeners.remove(listener);}public void notifyListeners (Consumer<? super ListenerType> algorithm) {// Execute some function on each of the listenersthis.listeners.forEach(algorithm);} }

    正因為沒有提供正在注冊的監聽器類型的接口信息,不能直接通知某個特定的監聽器,所以正需要保證通知功能的通用性,允許客戶端添加一些功能,如接受泛型參數類型的參數匹配,以適用于每個監聽器,具體實現代碼如下:

    public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> {private List<Animal> animals = new ArrayList<>();public void addAnimal (Animal animal) {// Add the animal to the list of animalsthis.animals.add(animal);// Notify the list of registered listenersthis.notifyListeners((listener) -> listener.updateAnimalAdded(animal));} }

    Mixin類技術的最大優勢是把觀察者模式的Subject封裝到一個可重復調用的類中,而不是在每個subject類中都重復寫這些邏輯。此外,這一方法使得zoo類的實現更為簡潔,只需要存儲動物信息,而不用再考慮如何存儲和通知監聽器。

    然而,使用mixin類并非只有優點。比如,如果要存儲多個類型的監聽器怎么辦?例如,還需要存儲監聽器類型AnimalRemovedListener。mixin類是抽象類,Java中不能同時繼承多個抽象類,而且mixin類不能改用接口實現,這是因為接口不包含state,而觀察者模式中state需要用來保存已經注冊的監聽器列表。

    其中的一個解決方案是創建一個動物增加和減少時都會通知的監聽器類型ZooListener,代碼如下所示:

    public interface ZooListener {public void onAnimalAdded (Animal animal);public void onAnimalRemoved (Animal animal); }

    這樣就可以使用該接口實現利用一個監聽器類型對zoo狀態各種變化的監聽了:

    public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> {private List<Animal> animals = new ArrayList<>();public void addAnimal (Animal animal) {// Add the animal to the list of animalsthis.animals.add(animal);// Notify the list of registered listenersthis.notifyListeners((listener) -> listener.onAnimalAdded(animal));}public void removeAnimal (Animal animal) {// Remove the animal from the list of animalsthis.animals.remove(animal);// Notify the list of registered listenersthis.notifyListeners((listener) -> listener.onAnimalRemoved(animal));} }

    將多個監聽器類型合并到一個監聽器接口中確實解決了上面提到的問題,但仍舊存在不足之處,接下來的章節會詳細討論。

    ##Multi-Method監聽器和適配器

    在上述方法,監聽器的接口中實現的包含太多函數,接口就過于冗長,例如,Swing MouseListener就包含5個必要的函數。盡管可能只會用到其中一個,但是只要用到鼠標點擊事件就必須要添加這5個函數,更多可能是用空函數體來實現剩下的函數,這無疑會給代碼帶來不必要的混亂。

    其中一種解決方案是創建適配器(概念來自GoF提出的適配器模式),適配器中以抽象函數的形式實現監聽器接口的操作,供具體監聽器類繼承。這樣一來,具體監聽器類就可以選擇其需要的函數,對adapter不需要的函數采用默認操作即可。例如上面例子中的ZooListener類,創建ZooAdapter(Adapter的命名規則與監聽器一致,只需要把類名中的Listener改為Adapter即可),代碼如下:

    public class ZooAdapter implements ZooListener {@Overridepublic void onAnimalAdded (Animal animal) {}@Overridepublic void onAnimalRemoved (Animal animal) {} }

    乍一看,這個適配器類微不足道,然而它所帶來的便利卻是不可小覷的。比如對于下面的具體類,只需選擇對其實現有用的函數即可:

    public class NamePrinterZooAdapter extends ZooAdapter {@Overridepublic void onAnimalAdded (Animal animal) {// Print the name of the animal that was addedSystem.out.println("Added animal named " + animal.getName());} }

    有兩種替代方案同樣可以實現適配器類的功能:一是使用默認函數;二是把監聽器接口和適配器類合并到一個具體類中。默認函數是Java8新提出的,在接口中允許開發者提供默認(防御)的實現方法。

    Java庫的這一更新主要是方便開發者在不改變老版本代碼的情況下,實現程序擴展,因此應該慎用這個方法。部分開發者多次使用后,會感覺這樣寫的代碼不夠專業,而又有開發者認為這是Java8的特色,不管怎樣,需要明白這個技術提出的初衷是什么,再結合具體問題決定是否要用。使用默認函數實現的ZooListener接口代碼如下示:

    public interface ZooListener {default public void onAnimalAdded (Animal animal) {}default public void onAnimalRemoved (Animal animal) {} }

    通過使用默認函數,實現該接口的具體類,無需在接口中實現全部函數,而是選擇性實現所需函數。雖然這是接口膨脹問題一個較為簡潔的解決方案,開發者在使用時還應多加注意。

    第二種方案是簡化觀察者模式,省略了監聽器接口,而是用具體類實現監聽器的功能。比如ZooListener接口就變成了下面這樣:

    public class ZooListener {public void onAnimalAdded (Animal animal) {}public void onAnimalRemoved (Animal animal) {} }

    這一方案簡化了觀察者模式的層次結構,但它并非適用于所有情況,因為如果把監聽器接口合并到具體類中,具體監聽器就不可以實現多個監聽接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口寫在同一個具體類中,那么單獨一個具體監聽器就不可以同時實現這兩個接口了。此外,監聽器接口的意圖比具體類更顯而易見,很顯然前者就是為其他類提供接口,但后者就并非那么明顯了。

    如果沒有合適的文檔說明,開發者并不會知道已經有一個類扮演著接口的角色,實現了其對應的所有函數。此外,類名不包含adapter,因為類并不適配于某一個接口,因此類名并沒有特別暗示此意圖。綜上所述,特定問題需要選擇特定的方法,并沒有哪個方法是萬能的。

    在開始下一章前,需要特別提一下,適配器在觀察模式中很常見,尤其是在老版本的Java代碼中。Swing API正是以適配器為基礎實現的,正如很多老應用在Java5和Java6中的觀察者模式中所使用的那樣。zoo案例中的監聽器或許并不需要適配器,但需要了解適配器提出的目的以及其應用,因為我們可以在現有的代碼中對其進行使用。下面的章節,將會介紹時間復雜的監聽器,該類監聽器可能會執行耗時的運算或進行異步調用,不能立即給出返回值。

    ##Complex & Blocking監聽器

    關于觀察者模式的一個假設是:執行一個函數時,一系列監聽器會被調用,但假定這一過程對調用者而言是完全透明的。例如,客戶端代碼在Zoo中添加animal時,在返回添加成功之前,并不知道會調用一系列監聽器。如果監聽器的執行需要時間較長(其時間受監聽器的數量、每個監聽器執行時間影響),那么客戶端代碼將會感知這一簡單增加動物操作的時間副作用。

    本文不能面面俱到的討論這個話題,下面幾條是開發者調用復雜的監聽器時應該注意的事項:

  • 監聽器啟動新線程。新線程啟動后,在新線程中執行監聽器邏輯的同時,返回監聽器函數的處理結果,并運行其他監聽器執行。

  • Subject啟動新線程。與傳統的線性迭代已注冊的監聽器列表不同,Subject的notify函數重啟一個新的線程,然后在新線程中迭代監聽器列表。這樣使得notify函數在執行其他監聽器操作的同時可以輸出其返回值。需要注意的是需要一個線程安全機制來確保監聽器列表不會進行并發修改。

  • 隊列化監聽器調用并采用一組線程執行監聽功能。將監聽器操作封裝在一些函數中并隊列化這些函數,而非簡單的迭代調用監聽器列表。這些監聽器存儲到隊列中后,線程就可以從隊列中彈出單個元素并執行其監聽邏輯。這類似于生產者-消費者問題,notify過程產生可執行函數隊列,然后線程依次從隊列中取出并執行這些函數,函數需要存儲被創建的時間而非執行的時間供監聽器函數調用。例如,監聽器被調用時創建的函數,那么該函數就需要存儲該時間點,這一功能類似于Java中的如下操作:

  • public class AnimalAddedFunctor {private final AnimalAddedListener listener;private final Animal parameter;public AnimalAddedFunctor (AnimalAddedListener listener, Animal parameter) {this.listener = listener;this.parameter = parameter;}public void execute () {// Execute the listener with the parameter provided during creationthis.listener.updateAnimalAdded(this.parameter);} }

    函數創建并保存在隊列中,可以隨時調用,這樣一來就無需在遍歷監聽器列表時立即執行其對應操作了。一旦每個激活監聽器的函數都壓入隊列中,“消費者線程”就會給客戶端代碼返回操作權。之后某個時間點“消費者線程”將會執行這些函數,就像在監聽器被notify函數激活時執行一樣。這項技術在其他語言中被叫作參數綁定,剛好適合上面的例子,技術的實質是保存監聽器的參數,execute()函數再直接調用。如果監聽器接收多個參數,處理方法也類似。

    需要注意的是如果要保存監聽器的執行順序,則需要引入綜合排序機制。方案一中,監聽器按照正常的順序激活新線程,這樣可以確保監聽器按照注冊的順序執行。方案二中,隊列支持排序,其中的函數會按照進入隊列的順序執行。簡單來說就是,開發者需要重視監聽器多線程執行的復雜程度,加以小心處理以確保實現所需的功能。

    ##結束語

    觀察者模式在1994年被寫進書中以前,就已經是主流的軟件設計模式了,為軟件設計中經常出現的問題提供了很多令人滿意的解決方案。Java一直是使用該模式的引領者,在其標準庫中封裝了這一模式,但是鑒于Java更新到了版本8,十分有必要重新考查經典模式在其中的使用。隨著lambda表達式和其他新結構的出現,這一“古老的”模式又有了新的生機。無論是處理舊程序還是使用這一歷史悠久的方法解決新問題,尤其對經驗豐富的Java開發者來說,觀察者模式都是開發者的主要工具。

    (編譯自:https://dzone.com/articles/the-observer-pattern-using-modern-java)

    OneAPM 為您提供端到端的 Java 應用性能解決方案,我們支持所有常見的 Java 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監控從來沒有如此簡單。想技術文章,請訪問 OneAPM 官方技術博客。 本文轉自 OneAPM 官方博客

    轉載于:https://my.oschina.net/oneapmofficial/blog/620818

    總結

    以上是生活随笔為你收集整理的如何使用 Java8 实现观察者模式?(下)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。