漫話:如何給女友解釋什麼是單例模式?

週末了,臨近五一勞動節,女友尚未想好要去哪裏玩,還在看着各類攻略。我則在旁邊一邊看書默默的心疼着個人錢包。忽然女友開始發問:
java

什麼是單例

單例模式,也叫單子模式,是一種經常使用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。算法

許多時候整個系統只須要擁有一個的全局對象,這樣有利於咱們協調系統總體的行爲。好比在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,而後服務進程中的其餘對象再經過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。設計模式

舉個簡單的例子,就像中國的一夫一妻制度,夫妻之間只能是一對一的,也就是說,一個男子同時只能有一個老婆。這種狀況就叫作單例。在中國,是經過《婚姻法》來限制一夫一妻制的。安全

男女雙方來到民政局登記
if 男方目前已經有老婆{
    提醒二位沒法結婚。並告知其當前老婆是誰。
}else{
    檢查女方婚姻情況,其餘基本信息覈實。
    贊成雙方結爲夫妻。
}
複製代碼

對於代碼開發中,一個類同時只有一個實例對象的狀況就叫作單例。那麼,如何保證一個類只能有一個對象呢?bash

咱們知道,在面向對象的思想中,經過類的構造函數能夠建立對象,只要內存足夠,能夠建立任意個對象。服務器

因此,要想限制某一個類只有一個單例對象,就須要在他的構造函數上下功夫。多線程

實現對象單例模式的思路是:併發

一、一個類能返回對象一個引用(永遠是同一個)和一個得到該實例的方法(必須是靜態方法,一般使用getInstance這個名稱);函數

二、當咱們調用這個方法時,若是類持有的引用不爲空就返回這個引用,若是類保持的引用爲空就建立該類的實例並將實例的引用賦予該類保持的引用;性能

三、同時咱們還將該類的構造函數定義爲私有方法,這樣其餘處的代碼就沒法經過調用該類的構造函數來實例化該類的對象,只有經過該類提供的靜態方法來獲得該類的惟一實例。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  
複製代碼

以上Java代碼,就實現了一個簡單的單例模式。咱們經過將構造方法定義爲私有,而後提供一個getInstance方法,該方法中來判斷是否已經存在該類的實例,若是存在直接返回。若是不存在則建立一個再返回。

線程安全的單例

關於併發,能夠參考《如何給女友解釋什麼是並行和併發》

在中國,想要擁有一個妻子,須要男女雙方帶着各自的戶口本一塊兒去民政局領證。民政局的工做人員會先在系統中查詢雙方的婚姻情況,而後再辦理登記手續。之因此能夠保證一夫一妻登記成功的前提是不會發生併發問題。

假設某男子能夠作到在同一時間分別和兩個不一樣的女子來登記,就有一種機率是當工做人員查詢的時候他並無結婚,而後就可能給他登記兩次結婚。固然,這種狀況在現實生活中是根本不可能發生的。

可是,在程序中,一旦有多線程場景,這種狀況就很常見。就像上面的代碼。

若是有兩個線程同時執行到if(instance==null)這行代碼,這是判斷都會經過,而後各自會執行instance = new Singleton();並各自返回一個instance,這時候就產生了多個實例,就沒有保證單例!

上面這種單例的實現方式咱們一般稱之爲懶漢模式,所謂懶漢,指的是隻有在須要對象的時候纔會生成(getInstance方法被調用的時候纔會生成)。這有點像現實生活中有一種"生米煮成熟飯"的狀況,到了必定要結婚的時候纔開始去領證。

上面的這種懶漢模式並非線程安全的,因此並不建議在平常開發中使用。基於這種模式,咱們能夠實現一個線程安全的單例的,以下:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  
複製代碼

經過在getInstance方法上增長synchronized,經過鎖來解決併發問題。這種實現方式就不會發生有多個對象被建立的問題了。

雙重校驗鎖

上面這種線程安全的懶漢寫法可以在多線程中很好的工做,可是,遺憾的是,這種作法效率很低,由於只有第一次初始化的時候才須要進行併發控制,大多數狀況下是不須要同步的。

咱們其實能夠把上述代碼作一些優化的,由於懶漢模式中使用synchronized定義一個同步方法,咱們知道,synchronized還能夠用來定義同步代碼塊,而同步代碼塊的粒度要比同步方法小一些,從而效率就會高一些。如如下代碼:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
複製代碼

上面這種形式,只有在singleton == null的狀況下再進行加鎖建立對象,若是singleton!=null的話,就直接返回就好了,並無進行併發控制。大大的提高了效率。

從上面的代碼中能夠看到,其實整個過程當中進行了兩次singleton == null的判斷,因此這種方法被稱之爲"雙重校驗鎖"。

還有值得注意的是,雙重校驗鎖的實現方式中,靜態成員變量singleton必須經過volatile來修飾,保證其初始化的原子性,不然可能被引用到一個未初始化完成的對象。

爲何雙重校驗鎖須要使用volatile來修飾靜態成員變量singleton?爲何線程安全的懶漢就不須要呢?關於這個問題,後續文章深刻講解。

餓漢模式

前面提到的懶漢模式,實際上是一種lazy-loading思想的實踐,這種實現有一個比較大的好處,就是隻有真正用到的時候才建立,若是沒被使用到,就一直不會被建立,這就避免了沒必要要的開銷。

可是這種作法,其實也有一個小缺點,就是第一次使用的時候,須要進行初始化操做,可能會有比較高的耗時。若是是已知某一個對象必定會使用到的話,其實能夠採用一種餓漢的實現方式。

所謂餓漢,就是事先準備好,須要的時候直接給你就好了。這就是平常中比較常見的"先買票後上車",走正常的手續。

如如下代碼,餓漢模式:

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}   
複製代碼

或者如下代碼,餓漢變種:

public class Singleton {  
    private Singleton instance = null;  
    static {  
    instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return this.instance;  
    }  
}  
複製代碼

以上兩段代碼其實沒有本質的區別,都是經過static來實例化類對象。餓漢模式中的靜態變量是隨着類加載時被完成初始化的。餓漢變種中的靜態代碼塊也會隨着類的加載一塊執行。

以上兩個餓漢方法,其實都是經過定義靜態的成員變量,以保證instance能夠在類初始化的時候被實例化。

由於類的初始化是由ClassLoader完成的,這實際上是利用了ClassLoader的線程安全機制。ClassLoader的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是由於這樣, 除非被重寫,這個方法默認在整個裝載過程當中都是同步的(線程安全的)

除了以上兩種餓漢方式,還有一種實現方式也是藉助了calss的初始化來實現的,那就是經過靜態內部類來實現的單例:

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}  
複製代碼

前面提到的餓漢模式,只要Singleton類被裝載了,那麼instance就會被實例化。

而這種方式是Singleton類被裝載了,instance不必定被初始化。由於SingletonHolder類沒有被主動使用,只有顯示經過調用getInstance方法時,纔會顯示裝載SingletonHolder類,從而實例化instance。

使用靜態內部類,藉助了classloader來實現了線程安全,這與餓漢模式有着殊途同歸之妙,可是他有兼顧了懶漢模式的lazy-loading功能,相比較之下,有很大優點。

單例的破壞

前文介紹過,咱們實現的單例,把構造方法設置爲私有方法來避免外部調用是很重要的一個前提。可是,私有的構造方法外部真的就徹底不能調用了麼?

其實不是的,咱們是能夠經過反射來調用類中的私有方法的,構造方法也不例外,因此,咱們能夠經過反射來破壞單例。

除了這種狀況,還有一種比較容易被忽視的狀況,那就是其實對象的序列化和反序列化也會破壞單例。

如使用ObjectInputStream進行反序列化時,在ObjectInputStream的readObject生成對象的過程當中,其實會經過反射的方式調用無參構造方法新建一個對象。

因此,在對單例對象進行序列化以及反序列化的時候,必定要考慮到這種單例可能被破壞的狀況。

能夠經過在Singleton類中定義readResolve的方式,解決該問題:

/**
 * 使用雙重校驗鎖方式實現單例
 */
public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
} 
複製代碼

枚舉實現單例

在StakcOverflow中,有一個關於What is an efficient way to implement a singleton pattern in Java?的討論:

如上圖,得票率最高的回答是:使用枚舉。

回答者引用了Joshua Bloch大神在《Effective Java》中明確表達過的觀點:

使用枚舉實現單例的方法雖然尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。

若是你真的深刻理解了單例的用法以及一些可能存在的坑的話,那麼你也許也能獲得相同的結論,那就是:使用枚舉實現單例是一種很好的方法。

枚舉實現單例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  
複製代碼

以上,就實現了一個很是簡單的單例,從代碼行數上看,他比以前介紹過的任何一種都要精簡,而且,他仍是線程安全的。

這些,其實還不足以說服咱們這種方式最優。可是還有個相當重要的緣由,那就是:枚舉可解決反序列化會破壞單例的問題

關於這個知識點,你們能夠參考《爲何我牆裂建議你們使用枚舉來實現單例》這篇文章,裏面詳細的闡述了關於枚舉與單例的全部知識點。

不使用synchronized實現單例

前面講過的全部方式,只要是線程安全的,其實都直接或者間接用到了synchronized,那麼,若是不能使用synchronized的話,怎麼實現單例呢?

使用Lock?這固然能夠了,可是其實根本仍是加鎖,有沒有不用鎖的方式呢?

答案是有的,那就是CAS。CAS是一項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。

在JDK1.5 中新增java.util.concurrent(J.U.C)就是創建在CAS之上的。相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。因此J.U.C在性能上有了很大的提高。

藉助CAS(AtomicReference)實現單例模式:

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}
複製代碼

用CAS的好處在於不須要使用傳統的鎖機制來保證線程安全,CAS是一種基於忙等待的算法,依賴底層硬件的實現,相對於鎖它沒有線程切換和阻塞的額外消耗,能夠支持較大的並行度。

使用CAS實現單例只是個思路而已,只是拓展一下幫助讀者熟練掌握CAS以及單例等知識、千萬不要在代碼中使用!!!這個代碼其實有很大的優化空間。聰明的你,知道以上代碼存在哪些隱患嗎?

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息