【設計模式 - Java篇】:設計模式第一式,單例模式

前言

  • 單例模式的動機
    對於一個軟件系統的某些類而言,我們無須創建多個實例。舉個大家都熟知的例子——Windows任務管理器,我們可以做一個這樣的嘗試,在Windows的“任務欄”的右鍵彈出菜單上多次點擊“啓動任務管理器”,看能否打開多個任務管理器窗口?如果你的桌面出現多個任務管理器,我請你喫飯,微笑(注:電腦中毒或私自修改Windows內核者除外)。通常情況下,無論我們啓動任務管理多少次,Windows系統始終只能彈出一個任務管理器窗口,也就是說在一個Windows系統中,任務管理器存在唯一性。爲什麼要這樣設計呢?我們可以從以下兩個方面來分析:其一,如果能彈出多個窗口,且這些窗口的內容完全一致,全部是重複對象,這勢必會浪費系統資源,任務管理器需要獲取系統運行時的諸多信息,這些信息的獲取需要消耗一定的系統資源,包括CPU資源及內存資源等,浪費是可恥的,而且根本沒有必要顯示多個內容完全相同的窗口;其二,如果彈出的多個窗口內容不一致,問題就更加嚴重了,這意味着在某一瞬間系統資源使用情況和進程、服務等信息存在多個狀態,例如任務管理器窗口A顯示“CPU使用率”爲10%,窗口B顯示“CPU使用率”爲15%,到底哪個纔是真實的呢?這純屬“調戲”用戶,偷笑,給用戶帶來誤解,更不可取。由此可見,確保Windows任務管理器在系統中有且僅有一個非常重要。

回到實際開發中,我們也經常遇到類似的情況,爲了節約系統資源,有時需要確保系統中某個類只有唯一一個實例,當這個唯一實例創建成功之後,我們無法再創建一個同類型的其他對象,所有的操作都只能基於這個唯一實例。爲了確保對象的唯一性,我們可以通過單例模式來實現,這就是單例模式的動機所在。

詳情

一. 兩種簡單的單例

1. 餓漢式單例類:

餓漢式單例類是實現起來最簡單的單例類,由於在定義靜態變量的時候實例化單例類,因此在類加載的時候就已經創建了單例對象,代碼如下所示:

class EagerSingleton {   
    private static final EagerSingleton instance = new EagerSingleton();   
    private EagerSingleton() { }   

    public static EagerSingleton getInstance() {  
        return instance;   
    }     
}

當類被加載時,靜態變量instance會被初始化,此時類的私有構造函數會被調用,單例類的唯一實例將被創建。如果使用餓漢式單例則不會出現創建多個單例對象的情況,可確保單例對象的唯一性。

2. 懶漢式單例類 與線程鎖定:

除了餓漢式單例,還有一種經典的懶漢式單例; 懶漢式單例在第一次調用getInstance()方法時實例化,在類加載時並不自行實例化,這種技術又稱爲延遲加載(Lazy Load)技術,即需要的時候再加載實例,爲了避免多個線程同時調用getInstance()方法,我們可以使用關鍵字synchronized,代碼如下所示:

class LazySingleton {   
    private static LazySingleton instance = null;   

    private LazySingleton() { }   

    synchronized public static LazySingleton getInstance() {   
        if (instance == null) {  
            instance = new LazySingleton();   
        }  
        return instance;   
    }  
}

2.1 簡單改進版本:
該懶漢式單例類在getInstance()方法前面增加了關鍵字synchronized進行線程鎖,以處理多個線程同時訪問的問題。但是,上述代碼雖然解決了線程安全問題,但是每次調用getInstance()時都需要進行線程鎖定判斷,在多線程高併發訪問環境中,將會導致系統性能大大降低。如何既解決線程安全問題又不影響系統性能呢?我們繼續對懶漢式單例進行改進。事實上,我們無須對整個getInstance()方法進行鎖定,只需對其中的代碼“instance = new LazySingleton();”進行鎖定即可。因此getInstance()方法可以進行如下改進:

public static LazySingleton getInstance() {   
    if (instance == null) {  
        synchronized (LazySingleton.class) {  
            instance = new LazySingleton();   
        }  
    }  
    return instance;   
}

2.2 再改進版本:
問題貌似得以解決,事實並非如此。如果使用以上代碼來實現單例,還是會存在單例對象不唯一。原因如下:

假如在某一瞬間線程A和線程B都在調用getInstance()方法,此時instance對象爲null值,均能通過instance == null的判斷。由於實現了synchronized加鎖機制,線程A進入synchronized鎖定的代碼中執行實例創建代碼,線程B處於排隊等待狀態,必須等待線程A執行完畢後纔可以進入synchronized鎖定代碼。但當A執行完畢時,線程B並不知道實例已經創建,將繼續創建新的實例,導致產生多個單例對象,違背單例模式的設計思想,因此需要進行進一步改進,在synchronized中再進行一次(instance == null)判斷,這種方式稱爲雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實現的懶漢式單例類完整代碼如下所示:

class DCLSingleton {   
    private volatile static DCLSingleton instance = null;   

    private DCLSingleton () { }   

    public static DCLSingleton getInstance() {   
        //第一重判斷  
        if (instance == null) {  
            //鎖定代碼塊  
            synchronized (DCLSingleton .class) {  
                //第二重判斷  
                if (instance == null) {  
                    instance = new DCLSingleton (); //創建單例實例  
                }  
            }  
        }  
        return instance;   
    }  
}

需要注意的是,如果使用雙重檢查鎖定來實現懶漢式單例類,需要在靜態成員變量instance之前增加修飾符volatile,被volatile修飾的成員變量可以確保多個線程都能夠正確處理,且該代碼只能在JDK 1.5及以上版本中才能正確執行。由於volatile關鍵字會屏蔽Java虛擬機所做的一些代碼優化 (防止指令重排),可能會導致系統運行效率降低,因此即使使用雙重檢查鎖定來實現單例模式也不是一種完美的實現方式。

3. 餓漢式單例類與懶漢式單例類比較

餓漢式單例類在類被加載時就將自己實例化,它的優點在於無須考慮多線程訪問問題,可以確保實例的唯一性;從調用速度和反應時間角度來講,由於單例對象一開始就得以創建,因此要優於懶漢式單例。但是無論系統在運行時是否需要使用該單例對象,由於在類加載時該對象就需要創建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統加載時由於需要創建餓漢式單例對象,加載時間可能會比較長。
懶漢式單例類在第一次使用時創建,無須一直佔用系統資源,實現了延遲加載,但是必須處理好多個線程同時訪問的問題,特別是當單例類作爲資源控制器,在實例化時必然涉及資源初始化,而資源初始化很有可能耗費大量時間,這意味着出現多線程同時首次引用此類的機率變得較大,需要通過雙重檢查鎖定等機制進行控制,這將導致系統性能受到一定影響。

二. 一種更好的單例實現方法

餓漢式單例類不能實現延遲加載,不管將來用不用始終佔據內存;懶漢式單例類線程安全控制煩瑣,而且性能受影響。可見,無論是餓漢式單例還是懶漢式單例都存在這樣那樣的問題,有沒有一種方法,能夠將兩種單例的缺點都克服,而將兩者的優點合二爲一呢?答案是:Yes!下面我們來學習這種更好的被稱之爲Initialization Demand Holder (IoDH)的技術。
在IoDH中,我們在單例類中增加一個靜態(static)內部類,在該內部類中創建單例對象,再將該單例對象通過getInstance()方法返回給外部使用,實現代碼如下所示:

//Initialization on Demand Holder  
class Singleton {  
    private Singleton() {  
    }  

    private static class HolderClass {  
            private final static Singleton instance = new Singleton();  
    }  

    public static Singleton getInstance() {  
        return HolderClass.instance;  
    }  

    public static void main(String args[]) {  
        Singleton s1, s2;   
        s1 = Singleton.getInstance();  
        s2 = Singleton.getInstance();  
        System.out.println(s1==s2);  
    }  
}

編譯並運行上述代碼,運行結果爲:true,即創建的單例對象s1和s2爲同一對象。由於靜態單例對象沒有作爲Singleton的成員變量直接實例化,因此類加載時不會實例化Singleton,第一次調用getInstance()時將加載內部類HolderClass,在該內部類中定義了一個static類型的變量instance,此時會首先初始化這個成員變量,由Java虛擬機來保證其線程安全性,確保該成員變量只能初始化一次。由於getInstance()方法沒有任何線程鎖定,因此其性能不會造成任何影響。
通過使用IoDH,我們既可以實現延遲加載,又可以保證線程安全,不影響系統性能,不失爲一種最好的Java語言單例模式實現方式(其缺點是與編程語言本身的特性相關,很多面向對象語言不支持IoDH)。

三. 總結

單例模式作爲一種目標明確、結構簡單、理解容易的設計模式,在軟件開發中使用頻率相當高,在很多應用軟件和框架中都得以廣泛應用。

1. 主要優點

單例模式的主要優點如下:
(1) 單例模式提供了對唯一實例的受控訪問。因爲單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
(2) 由於在系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬的對象單例模式無疑可以提高系統的性能。
(3) 允許可變數目的實例。基於單例模式我們可以進行擴展,使用與單例控制相似的方法來獲得指定個數的對象實例,既節省系統資源,又解決了單例單例對象共享過多有損性能的問題。

2. 主要缺點

單例模式的主要缺點如下:
(1) 由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難。
(2) 單例類的職責過重,在一定程度上違背了“單一職責原則”。因爲單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的創建和產品的本身的功能融合到一起。
(3) 現在很多面向對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,因此,如果實例化的共享對象長時間不被利用,系統會認爲它是垃圾,會自動銷燬並回收資源,下次利用時又將重新實例化,這將導致共享的單例對象狀態的丟失。

3. 適用場景

在以下情況下可以考慮使用單例模式:

  • 系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器或資源管理器,或者需要考慮資源消耗太大而只允許創建一個對象。
  • 客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。

四,常見面試題

1,什麼單例設計模式?有什麼特點?解決了什麼問題?
2,DCL ( 雙重檢查鎖 ) 的單例模式中,每個關鍵字的意義,volatile關鍵字有什麼作用?
3,你在實際工作中用過單例模式嗎?你會在什麼場景下考慮到比較適合用單例模式?

歡迎在評論區寫下您的回答哦~

參考資料 & 致謝

【1】《Java設計模式》- 劉偉
【2】單例模式-Singleton Pattern

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章