JDK 自帶的觀察者模式源碼分析以及和自定義實現的取捨

前言

總的結論就是:不推薦使用JDK自帶的觀察者API,而是自定義實現,可是能夠借鑑其好的思想。java

java.util.Observer 接口源碼分析

該接口十分簡單,是各個觀察者須要實現的接口算法

package java.util;

public interface Observer {
    void update(Observable o, Object arg);
} 

借鑑 JDK 封裝方法的過多參數的方案

也十分直接,就是使用頂級父類 Object 作參數類型,而後本身能夠定義一個參數封裝的類。數組

另外,第一個參數 Observable 就是所謂的主題接口,JDK 給實現了,可是實現的不是特別好。估計 Oracle 也懶得改了,這裏加上這個參數,很是好,由於能讓觀察者知道究竟是哪一個主題通知的「我」。安全

java.util.Observable 類源碼分析

這是該API最大的問題,它使用的是類,而不是接口去擴展。微信

以下源碼,刪除了大量英文註釋,改成更精簡的形式,並加入了詳盡的批註併發

import java.util.Observer;
import java.util.Vector;

/**
 * 這裏其實就是主題接口的角色,只不過 JDK 的實現很爛——居然用類封裝的,這是公認的槽點之一。
 */
public class Observable {
    private boolean changed = false;
    private Vector obs; // 看到這裏,其實也知道,這個API不只太古老,並且還沒人維護了,用的仍是最老的,被淘汰的 Vector 實現的動態數組

    public Observable() {
        obs = new Vector();
    }

    // 註冊觀察者,線程安全,這是優勢之一,能夠借鑑
    public synchronized void addObserver(Observer o) {
        if (o == null) // 提升代碼健壯性
            throw new NullPointerException();
        if (!obs.contains(o)) { // 註冊時會去重,自定義實現須要注意也去重
            obs.addElement(o);
        }
    }

    // 觀察者取消註冊
    public synchronized void deleteObserver(Observer o) {
        obs.removeElement(o);
    }

    // 基於拉模型的通知方法
    public void notifyObservers() {
        notifyObservers(null);
    }

    // 基於推模型
    public void notifyObservers(Object arg) {
        // 一個臨時數組,用於併發訪問被觀察者時,保存觀察者列表的當前狀態——這就是基於備忘錄模式的簡單應用。
        Object[] arrLocal;
        // 在獲取到觀察者列表以前,不容許其餘線程改變觀察者列表
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            // 重置變化標記位爲 false
            clearChanged();
        }

        // 主題類釋放鎖,可是並不影響線程安全,由於加鎖以前已經將觀察者列表複製到臨時數組 arrLocal
        // 在通知時咱們只通知數組中的觀察者,當前刪除和添加觀察者,都不會影響咱們通知的對象
        for (int i = arrLocal.length - 1; i >= 0; i--)
            ((Observer) arrLocal[i]).update(this, arg);
    }

    public synchronized void deleteObservers() {
        obs.removeAllElements();
    }

    protected synchronized void setChanged() {
        changed = true;
    }

    protected synchronized void clearChanged() {
        changed = false;
    }

    public synchronized boolean hasChanged() {
        return changed;
    }

    public synchronized int countObservers() {
        return obs.size();
    }
}

setChanged 這個開關的做用——能夠借鑑思想

一、使得主題具有了很大的伸縮性源碼分析

假如沒有 setChanged,那麼一旦主題的狀態變了,就不得不當即通知訂閱者,這不是很合理,須要一個緩衝——setChanged,如JDK同樣,在notify方法中作判斷,若是狀態的變化達到了一個閾值,在設置 setChanged 條件,這時候纔會真的通知,這個條件以及閾值的設置能夠在主題類(繼承了Observalbe類)的業務代碼中實現。性能

JDK還提供了配套的檢查該標誌的方法。this

二、能篩選訂閱者spa

只有有效通知能夠調用 setChanged。好比,微信朋友圈的一條狀態,好友 A 點贊,後續該狀態的點贊和評論並非每條都通知 A,只有 A 的好友觸發的操做纔會通知A——好友纔會調用setChanged,這個業務邏輯就能夠借鑑JDK

三、能實現通知的撤銷

主題中能夠設置不少次的 setChanged,好比在一個事務中,在最後因爲某種緣由,事務失敗,那麼通知也必須取消,此時可使用 clearChanged 方法輕鬆解決問題

四、主題的主動權控制

setChanged 和 clearChanged 方法均爲 protected,而 notifyObservers 方法爲 public,這就致使存在外部隨意調用 notifyObservers 的可能,可是外部沒法調用 setChanged,所以真正的控制權屬於主題——即便外部能調用主題的通知方法,也是然並卵的

備忘錄模式的簡單應用——實現無鎖的線程安全

主題類即便在清理了狀態位以後就釋放鎖,可是並不影響通知方法的線程安全性,由於加鎖以前已經將觀察者的列表複製到了一個臨時數組 arrLocal——數組是不可變的,局部的。在釋放鎖後,通知觀察者們,可是隻通知該臨時數組中保存的觀察者的們快照,在通知的時候,即便有刪除和新的觀察者註冊,也不會影響通知的過程。

public void notifyObservers(Object arg) {
        // 一個臨時數組,用於併發訪問被觀察者時,保存觀察者列表的當前狀態——這就是基於備忘錄模式的簡單應用。
        Object[] arrLocal;
        // 在獲取到觀察者列表以前,不容許其餘線程改變觀察者列表
        synchronized (this) {
            if (!changed)
                return;
            arrLocal = obs.toArray();
            // 重置變化標記位爲 false
            clearChanged();
        }

        for (int i = arrLocal.length - 1; i >= 0; i--)
            ((Observer) arrLocal[i]).update(this, arg);
    }

通知方法中的缺陷

上面的實現中,能夠發現一個問題,update 是觀察者接口中的方法,是各個具體的觀察者須要實現的方法,若是具體觀察者的 update 方法有機會拋出異常,那麼若是 RD 沒有捕獲,就會把異常拋出,致使整個通知過程失敗,這裏也是爲何,不推薦使用該接口。

在本身實現的時候,能夠把觀察者的 update 方法,用異常控制塊包起來,保證通知過程能完整執行。

OOM 的隱患(微不足道)

主題也持有了觀察者的引用,若是未正常處理——及時的從主題中刪除廢棄的觀察者,會致使大量的廢棄觀察者沒法被回收。這裏其實主要仍是業務代碼的問題。

若是觀察者具體實現代碼有問題,可能會致使主題和觀察者對象造成循環引用,在某些採用引用計數的垃圾回收器可能致使沒法回收。可是,現代GC中,這種問題不會出現,引用計數器算法早已經被放棄使用。

持有觀察者的集合類 Vector 的性能問題

先說結論——Vector是最舊的 List 實現,再也不推薦使用。

這又是一個槽點,當初實現 JDK 的觀察者 API 的時候,可能動態數組用 vector 實現比較好,可是如今早就是推薦使用 Arraylist 了。雖然,vector 與 ArrayList 類似,可是:

一、Vector 是線程安全的list集合

Vector 徹底基於 synchronized 實現同步,雖然它的操做與 ArrayList 幾乎同樣,可是不少時候咱們不須要那麼重的實現,畢竟加鎖會影響性能。故通常直接使用ArrayList,並且,必定要實現線程安全的動態數組,也輪不到用 Vector,可使用 JUC 中的 CopyOnWriteArraylist 等容器。或者用 Collections 類的同步 List 靜態方法來轉換爲同步List

二、Vector 的部分方法名太長,ArrayList 的對應實現方法名短些,便於閱讀,目前仍在使用 Vector 的軟件,基本都是爲了兼容舊庫和懶得改

三、Vector 的容量增加性能不好

Vector 是可變數組,初始 length 是 10 ,若是超過 length 時,會以 100% 比率增加 length,即變成20,因此存在內存浪費的現象,而 Arraylist 的 length 是以 50% 比率增長,因此相比來講,內存使用率較高

主題通知觀察者的順序很奇葩,有bug風險

看源碼得知,主題通知觀察者的順序,是 tmd 的倒敘,致使通知觀察者的順序和註冊的順序不同,若是業務代碼對順序有要求,就很差弄了

主題是類實現的,擴展難

衆所周知,Java 沒有多繼承機制,若是具體主題除了繼承主題類外, 還想繼承其餘業務類,就無法兒寫了。典型的違背了「組合(聚合)優於繼承的」設計原則。故通常自定義的實現比較多,也不難。雖然 JDK 給咱們作了封裝,可是不少不少時候,業務需求複雜,JDK 的 API 並不能知足咱們的需求。 

相關文章
相關標籤/搜索