Stories

Detail Return Return

《Head First設計模式》讀書筆記 —— 單例模式 - Stories Detail

《Head First設計模式》讀書筆記
相關代碼:Vks-Feng/HeadFirstDesignPatternNotes: Head First設計模式讀書筆記及相關代碼

用來創建獨一無二的,只能有一個實例的對象的入場券

為什麼需要單件模式

有些對象只能有一個實例

  • 線程池、緩存、對話框、設備的驅動程序的對象、註冊表設置對象
    如果製造出多個實例,就會導致許多問題產生
  • 程序的行為異常、資源使用過量、結果不一致

為什麼不去靠人為約定或全局變量實現該目標?

  • 人為約定:人為約定不一定能絕對嚴格地被遵守,且若存在更好方法,自然更願意接受
  • 全局變量:必須在程序開始前就創建對象並將其賦值給一個全局變量,若某次執行時沒用到則是資源的浪費。(不能實現“懶加載”)

單件模式典型實現剖析

public class Singleton {  
    private static Singleton uniqueInstance;  
  
    private Singleton() { }  
  
    public static Singleton getInstance() {  
        if (uniqueInstance == null) {  
            uniqueInstance = new Singleton();  
        }  
        return uniqueInstance;  
    }  
}

為什麼會被這樣設計?

  • 創建對象僅需簡單的new,為了避免被多次創建,將類的構造器私有化。
    • 但是私有化後只有類內的代碼才能實例化該類,而類不被實例化如何調用其構造器呢?(“雞生蛋,蛋生雞”)
  • 通過在類中設計一個靜態方法,解決不實例化該類,而能調用其構造器。

延遲實例化(Lazy instantiate)

if (uniqueInstance == null) {  
	uniqueInstance = new Singleton();  
}  
return uniqueInstance;

當我們需要該實例時再去創建對象,如果不需要就永遠不會產生。

定義單件模式

HeadFirst設計模式6-單件模式

單件模式確保一個類只有一個實例,並提供一個全局訪問點。

單件模式類圖.png

  • 把類設計成自己管理的一個單獨實例,同時也避免其他類再自行產生實例
  • 通過單間類事獲取單件實例的唯一途徑
  • 提供這個實例的全局訪問點:當你需要實例時,響雷查詢,它會返回單個實例
  • 延遲實例化的方式創建單件對資源敏感的對象特別重要

本節用例

某公司設計的巧克力鍋爐控制器
需要避免糟糕的情況發生:排除未煮沸的原料、已滿情況下繼續加原料、未放原料就空燒

public class ChocolateBoiler {  
    private boolean empty;  
    private boolean boiled;  
    public ChocolateBoiler() {  
        empty = true;  
        boiled = false;  
    }  
    public void fill() {  
        if (isEmpty()) {  
            empty = false;  
            boiled = false;  
            // 填充原料  
        }  
    }    public void drain() {  
        if (isEmpty() && isBoiled()) {  
            empty = true;  
        }  
    }    public void boil() {  
        if (!isEmpty() && !isBoiled()) {  
            boiled = true;  
        }  
    }  
    private boolean isBoiled() {  
        return boiled;  
    }  
  
    private boolean isEmpty() {  
        return empty;  
    }  
}

為了避免同時存在多個實例帶來問題,對其進行單件化改造

public class ChocolateBoiler {  
	private static ChocolateBoiler uniqueInstance;  
    private boolean empty;  
    private boolean boiled;  
    
    public static ChocolateBoiler getInstance() {
	    if (uniqueInstance == null) {
		    uniqueInstance = new ChocolateBoiler();
	    }
	    return uniqueInstance;
    }
    
    private ChocolateBoiler() {  
        empty = true;  
        boiled = false;  
    }  
}

多線程帶來的問題

當有多個熔爐實例時,可能會發生我們先前想避免的問題,但上面已有的單例模式可能會在多線程情況下創建出不只一個實例。

解決問題

只要把getInstance()變成同步(synchronized)方法,多線程災難幾乎就可以輕易地解決了

public class Singleton {  
    private static Singleton uniqueInstance;  
  
    private Singleton() { }  
  
    public static synchronized Singleton getInstance() {  
        if (uniqueInstance == null) {  
            uniqueInstance = new Singleton();  
        }  
        return uniqueInstance;  
    }  
}

問題:同步會降低性能,且當實例被正確初始化後,後續就不用再同步了,同步將會徹底成為累贅

優化

  1. getInstance()的性能對應用程序不是很關鍵,就什麼都別做
    • getInstance()被頻繁使用時,就需要考慮優化了
  2. 使用“急切”創建實例,而不用延遲實例化的做法
    • 頻繁使用getInstance()時可用
    • 創建和運行時負擔不繁重時可用
    public class Singleton {  
        private static Singleton uniqueInstance = new Singleton();  
      
        private Singleton() { }  
      
        public static synchronized Singleton getInstance() {  
            return uniqueInstance;  
        }  
    }
    
  3. “雙重檢查枷鎖”,在getInstance()中減少使用同步
    • 首先檢查是否實例已經創建了,如果尚未創建才會同步,這樣就可以實現“只有第一次同步”了
    private volatile static Singleton uniqueInstance;  
    
    private Singleton() { }  
    
    public static Singleton getInstance() {  
    	if (uniqueInstance == null) {  
            synchronized (Singleton.class) {  
                if (uniqueInstance == null) {  
                    uniqueInstance = new Singleton();  
                }  
            }    
        }    
        return uniqueInstance;  
    }
    
    • volatile關鍵字確保當uniqueInstance變量被初始化成Singleton實例時,多個線程正確地處理uniqueInstance變量

Q&A

Q:為何不創建一個類,將其所有方法和變量都定義為靜態的,直接把類當作一個單件?
A:

  • 如果類自給自足,而且不依賴於複雜的初始化,可以這麼做。
  • 但是因為靜態初始化的控制權是在Java手上,這麼做有可能導致混亂,特別是有許多類牽涉其中時。這麼做容易造成一些不易發現的和初始化次序有關的bug。
  • 建議使用對象的單件,比較保險。

Q:類加載器(class loader),聽説兩個類加載器可能有機會創建自己的單件實例
A:是的

  • 每個類加載器都定義了一個命名空間,如果有兩個以上的類加載器,不同的加載器可能會加載同一個類,從整個程序來看,同一個類會被加載多次。
  • 如果這種情況發生在單件上,就會產生多個單件並存的怪異現象。
  • 當程序有多個類加載器而你又使用了單件模式時,需要注意。解決方案:自行指定類加載器,並指定同一個類加載器

Q:可不可以繼承單件類?
A:繼承單間類面臨的問題:構造器是私有的。不能用私有構造器來擴展類。所以你必須把單件的構造器改成公開的或受保護的,而這樣:

  • 就算不上“真正的”單件了,因為別的類也可實例化它
  • 單件的實現是利用靜態變量,直接繼承會導致所有的派生類共享同一個實例變量,可能並非預期效果,需要實現註冊表(Registry)功能

Q:為什麼全局變量比單件模式差?
A:

  • Java中,全局變量基本上就是對對象的靜態引用。這種情況下使用全局變量就會有缺點,例如前文提到的急切實例化v.s.延遲實例化。
  • 但要記住該模式的目的“確保只有一個實例並提供全局訪問。全局變量可以提供全局訪問,但不能確保只有一個實例。全局變量也會變相鼓勵開發者用許多全局變量指向許多小對象來造成命名空間的污染。

總結

OO基礎

  • 抽象
  • 封裝
  • 多態
  • 繼承

OO原則

  • 封裝變化
  • 多用組合,少用繼承
  • 針對接口編程,不針對實現編程
  • 為交互對象之間的鬆耦合設計而努力
  • 對擴展開放,對修改關閉
  • 依賴抽象,不要依賴具體類

OO模式

  • 單件模式——確保一個類只有一個實例,並提供全局訪問。

要點

  • 單件模式確保程序中的類最多隻有一個實例
  • 單件模式也提供訪問這個實例的全局點
  • Java中實現單件模式需要私有的構造器、一個靜態方法和一個靜態變量
  • 確定在性能和資源上的限制,然後小心地選擇適當的方案來實現單件,以解決多線程的問題
  • 如果使用多個類加載器,可能導致單件失效而產生多個實例
user avatar dalideshoushudao Avatar free_like_bird Avatar shanliangdeshou_ccwzfd Avatar FreakEmbedded Avatar xingchenheyue Avatar
Favorites 5 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.