設計模式之單例模式

前言

作爲一個好學習的程序開發者,應該會去學習優秀的開源框架,當然學習的過程中不免會去閱讀源碼,這也是一個優秀程序員的必備素養,在學習的過程中很多人會遇到的障礙,那就是設計模式。很多優秀的框架會運用設計模式來達到事半功倍的效果。鑑於自己之前對設計模式的生疏,在閱讀源碼時遇到設計模式的巧妙運用理解比較吃力。最近搞了一本新書 《圖解設計模式》(目測講的很基礎)開始學習設計模式,對今後學習源碼打下堅實的基礎。後續我在閱讀本書的過程中,我將記錄下自己學習總結。今天就從最常使用的單例模式說起。

單例模式

在許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行爲。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。(維基百科)。

懶漢式

在單例模式中,有一種稱爲懶漢式的單例模式。顧名思義,懶漢式可以理解使用時才進行初始化,它包括私有的構造方法,私有的全局靜態變量,公有的靜態方法,是一種懶加載機制。

public class Singleton {

    private static Singleton instance;

    private Singleton() {
        System.out.println("初始化");
    }

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

上面是最常見的懶漢式單例模式的寫法,但是如果在多線程的情況下,上述方法就會出現問題,它達不到只有一個單例對象的效果,例如當某個線程1調用getInstance()方法並判斷instance == null
,此時(就在判斷爲空後new Singleton()之前)另一個線程2也調用getInstance()方法,由於此時線程1還沒有new出對象,則線程2執行getInstance()中instance 也爲空,那麼此時就會出現多個實例的情況,而達不到只有一個實例的目的。

懶漢式(線程安全)

在上述實現中我們提到的懶漢式單例模式是一種非線程安全的,非線程安全即多線程訪問時會生成多個實例。那麼怎麼樣實現線程安全呢,也許你應該已經想到使用同步關鍵字synchronized。

public class Singleton {

    private static Singleton instance;

    private Singleton() {
        System.out.println("初始化");
    }

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

使用同步關鍵字後,也就實現了線程安全訪問,因爲在任何時候它只能有一個線程調用 getInstance() 方法。那麼你可能會發出疑問,這樣加入同步,在高併發情況下,效率是很低的,因爲真正需要同步的是我們第一次初始化的時候,是的,所以我們要進行進一步的優化。

雙重檢測機制

雙重檢測顧名思義就是兩次檢測,一次是檢測instance 實例是否爲空,進行第一次過濾,在同步快中進行第二次檢測,因爲當多個線程執行第一次檢測通過後會同時進入同步快,那麼此時就有必要進行第二次檢測來避免生成多個實例。

public class Singleton {

    private static Singleton instance;

    private Singleton() {
        System.out.println("初始化");
    }

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

對於上面的代碼時近乎完美的,既然說近乎完美,那肯定還是有瑕疵的,瑕疵出現的原因就是instance = new Singleton();這一句代碼,你可能會問,這會有什麼問題,其實我也不知道,哈哈。在計算機語言中,初始化包含了三個步驟

  1. 分配內存
  2. 執行構造方法初始化
  3. 將對象指向分配的內存空間

由於java編譯器爲了儘可能減少內存操作速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規則(這規則後面再敘述)將程序編寫順序打亂——即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行——以儘可能充分地利用CPU就會出現指令重排序(happen-before),從而導致上面的三個步驟執行順序發生改變。正常情況下是123,但是如果指令重排後執行爲1,3,2那麼久會導致instance 爲空,進而導致程序出現問題。

既然已經知道了上述雙重檢測機制會出現問題,那麼我們該怎麼避免出現呢,該如何解決呢,最好的辦法就是不要使用,開個玩笑啦。java中有一個關鍵字volatile,他有一個作用就是防止指令重排序,那麼我們把singleton用volatile修飾下就可以了,如下。

private volatile static Singleton singleton;

餓漢式

餓漢式與懶漢式區別是它在類加載的時候就進行初始化操作,而懶漢式是調用getInstance()方法時才進行初始化。

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

    private Singleton() {
        System.out.println("初始化");
    }

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

與懶漢式相比,它是線程安全的(無需用同步關鍵字修飾),由於沒有加鎖,執行效率也相對較高,但是也有一些缺點,在類加載時就初始化,會浪費內存。

靜態塊實現方式

public class HungrySingleton implements Serializable {
    private static  HungrySingleton instance = null;
    static {
        instance = new HungrySingleton();
    }
    private HungrySingleton() {
        System.out.println("初始化");
    }

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

靜態內部類實現方式

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
        System.out.println("初始化");
    }

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚舉方式

public enum EnumSinglton {
    INSTANCE;

    private EnumSinglton() {
        System.out.println("構造方法");
    }
    public void  doSomething() {
        System.out.println("調用單例方法");
    }

}

枚舉方式實現的單例模式是一種線程安全的單例模式。

序列化對單例模式的影響

通過上面我們對單例模式的學習,對單例模式有了進一步的學習,當我們有序列化的需求之後,那麼會產生怎樣的效果呢?先通過下面的代碼來看下結果

public class Main {
    public static void main(String[] args) throws Exception {

        //測試序列化對單例模式的影響
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(LazySingleton.getInstance());
        ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
        LazySingleton singleton = (LazySingleton) new ObjectInputStream(bin).readObject();
        System.out.println(singleton == LazySingleton.getInstance());//false
        System.out.println( EnumSinglton.INSTANCE == EnumSinglton.INSTANCE );//true
    }
}

通過我們的測試瞬時就懵逼了,除了通過枚舉方式實現的單例模式,其它幾種實現都生成了多個實例。那麼該如何解決呢?難道反序列話只能生成多個實例。當然不是。我們可以看下面修改後的代碼

public class HungrySingleton implements Serializable {
    private static  HungrySingleton instance = new HungrySingleton();
    //餓漢式變種5
    static {
        instance = new HungrySingleton();
    }
    private HungrySingleton() {
        System.out.println("初始化");
    }

    public static HungrySingleton getInstance(){
        return instance;
    }

    /**
     * 如果序列化,需要加入此方法,否則單例模式無效
     * @see java.io.ObjectStreamClass
     * @return
     */
    private Object readResolve() {
        return instance;
    }

}

靜態內部類相對實現較爲簡單,並且它是一種懶加載機制, 當Singleton 類被裝載了,instance 不一定被初始化。因爲 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,纔會顯示裝載 SingletonHolder 類,從而實例化 instance。

好了,關於單例模式到這裏已經介紹完畢了。有問題歡迎留言指出,Have a wonderful day .

發佈了47 篇原創文章 · 獲贊 42 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章