你真的會寫單例模式嗎——Java實現

餓漢法html


顧名思義,餓漢法就是在第一次引用該類的時候就建立對象實例,而無論實際是否須要建立。代碼以下:java

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

這樣作的好處是編寫簡單,可是沒法作到延遲建立對象。可是咱們不少時候都但願對象能夠儘量地延遲加載,從而減少負載,因此就須要下面的懶漢法:android

單線程寫法

這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對singleton進行null判斷,若是是null就new一個出 來,最後返回singleton對象。這種方法能夠實現延時加載,可是有一個致命弱點:線程不安全。若是有兩條線程同時調用getSingleton() 方法,就有很大可能致使重複建立對象。web

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) singleton = new Singleton();
        return singleton;
    }
}

考慮線程安全的寫法

這種寫法考慮了線程安全,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同時,對 singleton對象使用volatile關鍵字進行限制,保證其對全部線程的可見性,而且禁止對其進行指令重排序優化。如此便可從語義上保證這種單例 模式寫法是線程安全的。注意,這裏說的是語義上,實際使用中仍是存在小坑的,會在後文寫到。緩存

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

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

兼顧線程安全和效率的寫法

雖然上面這種寫法是能夠正確運行的,可是其效率低下,仍是沒法實際應用。由於每次調用getSingleton()方法,都必須在synchronized這裏進行排隊,而真正遇到須要new的狀況是很是少的。因此,就誕生了第三種寫法:安全

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

這種寫法被稱爲「雙重檢查鎖」,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似畫蛇添足,但實際上卻極大提高了 併發度,進而提高了性能。爲何能夠提升併發度呢?就像上文說的,在單例中new的狀況很是少,絕大多數都是能夠並行的讀操做。所以在加鎖前多進行一次 null檢查就能夠減小絕大多數的加鎖操做,執行效率提升的目的也就達到了。多線程

那麼,這種寫法是否是絕對安全呢?前面說了,從語義角度來看,並無什麼問題。可是其實仍是有坑。說這個坑以前咱們要先來看看volatile這個 關鍵字。其實這個關鍵字有兩層語義。第一層語義相信你們都比較熟悉,就是可見性。可見性指的是在一個線程中對該變量的修改會立刻由工做內存(Work Memory)寫回主內存(Main Memory),因此會立刻反應在其它線程的讀取操做中。順便一提,工做內存和主內存能夠近似理解爲實際電腦中的高速緩存和主存,工做內存是線程獨享的, 主存是線程共享的。volatile的第二層語義是禁止指令重排序優化。你們知道咱們寫的代碼(尤爲是多線程代碼),因爲編譯器優化,在實際執行的時候可 能與咱們編寫的順序不一樣。編譯器只保證程序執行結果與源代碼相同,卻不保證明際指令的順序與源代碼相同。這在單線程看起來沒什麼問題,然而一旦引入多線 程,這種亂序就可能致使嚴重問題。volatile關鍵字就能夠從語義上解決這個問題。併發

注意,前面反覆提到「從語義上講是沒有問題的」,可是很不幸,禁止指令重排優化這條語義直到jdk1.5之後才能正確工做。此前的JDK中即便將變 量聲明爲volatile也沒法徹底避免重排序所致使的問題。因此,在jdk1.5版本前,雙重檢查鎖形式的單例模式是沒法保證線程安全的。高併發

靜態內部類法

那麼,有沒有一種延時加載,而且能保證線程安全的簡單寫法呢?咱們能夠把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就建立對象,而且因爲靜態內部類只會被加載一次,因此這種寫法也是線程安全的:性能

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
    
    private Singleton(){}
        
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

可是,上面提到的全部實現方式都有兩個共同的缺點:

  • 都須要額外的工做(Serializable、transient、readResolve())來實現序列化,不然每次反序列化一個序列化的對象實例時都會建立一個新的實例。

  • 可能會有人使用反射強行調用咱們的私有構造器(若是要避免這種狀況,能夠修改構造器,讓它在建立第二個實例的時候拋異常)。

枚舉寫法

固然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚舉除了線程安全和防止反射強行調用構造器以外,還提供了自動序列化機制,防止反序列化的時候建立新的對象。所以,Effective Java推薦儘量地使用枚舉來實現單例。

總結

這篇文章發出去之後獲得許多反饋,這讓我受寵若驚,以爲應該再寫一點小結。代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不一樣的平臺、不一樣的開發環境(尤爲是jdk版本)下,天然有不一樣的最優解(或者說較優解)。
好比枚舉,雖然Effective Java中推薦使用,可是在Android平臺上倒是不被推薦的。在這篇Android Training中明確指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再好比雙重檢查鎖法,不能在jdk1.5以前使用,而在Android平臺上使用就比較放心了(通常Android都是jdk1.6以上了,不只修正了volatile的語義問題,還加入了很多鎖優化,使得多線程同步的開銷下降很多)。

最後,無論採起何種方案,請時刻牢記單例的三大要點:

  • 線程安全

  • 延遲加載

  • 序列化與反序列化安全

參考資料

《Effective Java(第二版)》
 《深刻理解Java虛擬機——JVM高級特性與最佳實踐(第二版)》

原文參考:http://www.tekbroaden.com/singleton-java.html

相關文章
相關標籤/搜索