設計模式之4--單例模式

單例模式

實際開發過程中,有時候需要確保系統中某個類只有唯一一個實例,當這個唯一的實例創建成功之後,我們無法再創建同類型的其他對象,之後所有操作都是基於這個唯一的實例。爲了實現這個目標,我們下面就學習下單例模式。

爲了實現類的唯一性,我們需要三個步驟對普通的類進行修改。

  1. 一般創建對象都是調用new來實例化某個類,爲了確保實例的唯一性我們需要禁止類的外部使用new來創建對象,那麼就需要把構造函數設置private屬性。
  2. 當把構造函數設置爲private後,類的外部無法再創建對象,但是在類的內部還是可以創建的。因此我們在類的內部創建並保存這個唯一的實例。所以需要在類的內部定義一個靜態的私有變量。
  3. 最後爲了保證成員變量的封裝行,我們把定義的私有變量的可見性也設置爲private,同時增加一個公有的靜態方法,通過靜態方法返回創建的私有變量。

單例模式定義:確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類成爲單例類,它提供全局訪問的方法。單例模式是一種對象創建型模式

單例模式有三個要點:一是類只有一個實例;而是它必須自行穿件這個實例;三是它必須自行向整個系統提供這個實例。

上圖是單例模式的結構圖,圖中我們可以看到構造函數Singleton()和靜態成員instance都是私有private的,而只有一個GetInstance()是public的,這個方法提供給系統來訪問得到唯一的實例對象。

實現代碼如下

class Singleton {
    private static Singleton instance = null;
    private Singleton() {};

    public static Singleton getInstance() {
            if(instance == null)
                instance = new Singletion();
            return instance;
    }
}

現在我們知道了單例模式設計的要點,但是後來人們在實際開發過程中發現按照上述代碼實現的話系統中可能還是會存在多個類實例,而這個是怎麼發生的呢?

原來在第一次調用getInstance方法的時候,判斷instance爲null那麼就會執行new Singleton()的代碼,但是在創建Singleton對象的時候需要執行大量的初始化工程,而如果在這個時候再次去調用getInstance方法的話,由於此時instance還未創建成功仍然爲null的,就會導致new Singleton()再次被執行。這就導致了系統中還是有可能會存在多個實例,違背了單例模式的初衷。爲了解決這個問題,後來大牛們又找到了解決方法。

  1. 餓漢式單例類

    餓漢式單例類會在類加載的時候就去創建實例對象

    class EagerSingleton {
        private static final EagerSingletion instance = new Singleton();
        private EagerSingleton(){};
    
        public static EagerSingleton getInstance() {
            return instance;
        }
    
    }    
  2. 懶漢式單例類

    上面介紹的餓漢式單例模式在類加載的時候就會去創建實例對象,這個對象會長期存在於內存中,而不考慮系統到底是否需要使用。而懶漢式單例類則是將創建對象的行爲延後,等到系統調用getInstance()真正獲取該實例對象執行的時候纔會去創建對象。但是考慮遲到多個線程同時訪問的問題,可以使用synchronized關鍵字。

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

    上面的方法通過synchronized關鍵字進行同步,但是在多線程高併發的環境下在每次調用getInstance的時候就會進行線程鎖定判斷,這會導致系統性能大大降低。因此我們不必對getInstance整個方法進行鎖定,而只是需要對new LazySingleton()代碼進行鎖定就好了。

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

    通過上面的方法貌似系統性能降低的問題解決了,但是實際上這種設計方法還是會導致系統中存在多個實例對象,沒有真正的做到唯一,原因還是一個線程A在創建對象的時候,另一個線程B會因爲synchronized而等待,但是等到A線程創建結束之後,B不知道對象已經創建過了,還是會進入到new的階段。因此這裏進一步修改,設計一種稱爲雙重檢查鎖定的方法。

    class LazySingleton {
        private volatile static LazySingleton instance = null;
        private LazySingleton() { };
    
        public static LazySingleton getInstance() {
            //第一重判斷
            if(instance == null) {
                synchronized(LazySingleton.class) {
                    //第二重判斷
                    if(instance == null)
                        instance = new LazySingleton();
                }
            }
            return instance;
        }
    }

    雙重檢查鎖定的方法需要設置靜態私有變量爲volatile屬性,保證變量被多個線程正確處理。

上面主要描述了餓漢式單例模式和懶漢式單例模式的實現方法,而它們也是各有優缺點的。餓漢式單例模式在類加載的時候就會創建實例對象,那麼這勢必導致加載過程時間變長,並且無論系統是否真正使用該對象,類的對象已經被創建了,消耗了系統資源。而懶漢式單例模式是推遲到執行的時候纔去創建對象,那麼這就需要同步好多個線程訪問的情況,保證系統中始終存在一個實例對象,爲了同步進行的線程鎖定,也勢必會影響到系統的性能。

下面我們介紹一個更好的單例實現方法,我們在單例類中增加一個靜態內部類,在該內部類創建單例對象,再將該單例對象通過getInstance方法返回給外部使用。Initialization on Demand Holder(IoDH)

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();
    }
}

通過靜態內部類的實現方式,我們將創建對象的時間延後,因爲單例對象沒有作爲Singleton的成員,因此類加載的時候不會實例化。而在第一次調用getInstance的時候會加載HolderClass內部類,在內部類中有一個instance的成員變量,那麼這時候纔會去創建Singleton對象,由Java虛擬機來保證線程安全性,確保成員變量只初始化一次。

總結:
單例模式提供了對唯一實例的受控訪問,保證實例對象在系統中是唯一的,對於一些需要頻繁創建和銷燬的對象以及消耗大量資源的對象來說單例對象可以提高系統的性能。
同時基於單例模式我們還可以擴展成可變數目的實例。

缺點:
單例模式沒有抽象層,因此擴展很麻煩。
對於Java這種由垃圾自動回收的語言,如果單例對象長時間不被使用,那麼可能會被系統回收,下次使用的時候再重新實例化,這就會導致之前的單例對象狀態丟失。

適用場景:

  1. 系統只需要一個實例對象,或者需要消耗資源太大而只允許創建一個對象。
  2. 客戶端調用類的單個實例只能通過一個公共訪問點,而不能通過其它途徑訪問該實例。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章