單例模式總結

第一種(懶漢, 線程不安全):java

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

這種寫法 lazy loading 很明顯, 可是致命的是在多線程不能正常工做。緩存

第二種(懶漢, 線程安全):安全

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

這種寫法可以在多線程中很好的工做, 並且看起來它也具有很好的 lazy loading, 可是, 遺憾的是, 效率很低, 99% 狀況下不須要同步。多線程

第三種(餓漢):函數

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

    private Singleton() {
    }

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

這種方式基於 classloder 機制避免了多線程的同步問題, 不過, instance 在類裝載時就實例化, 雖然致使類裝載的緣由有不少種, 在單例模式中大多數都是調用 getInstance 方法, 可是也不能肯定有其餘的方式(或者其餘的靜態方法)致使類裝載, 這時候初始化 instance 顯然沒有達到 lazy loading 的效果。優化

第四種(餓漢, 變種):this

public class Singleton {
    private Singleton instance = null;

    static {
        instance = new Singleton();
    }

    private Singleton() {
    }

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

表面上看起來差異挺大, 其實更第三種方式差很少, 都是在類初始化即實例化 instance。線程

第五種(靜態內部類):code

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

    private Singleton() {
    }

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

這種方式一樣利用了classloder的機制來保證初始化instance時只有一個線程, 它跟第三種和第四種方式不一樣的是(很細微的差異):第三種和第四種方式是隻要Singleton類被裝載了, 那麼instance就會被實例化(沒有達到lazy loading效果), 而這種方式是Singleton類被裝載了, instance不必定被初始化。由於SingletonHolder類沒有被主動使用, 只有顯示經過調用getInstance方法時, 纔會顯示裝載SingletonHolder類, 從而實例化instance。想象一下, 若是實例化instance很消耗資源, 我想讓他延遲加載, 另一方面, 我不但願在Singleton類加載時就實例化, 由於我不能確保Singleton類還可能在其餘的地方被主動使用從而被加載, 那麼這個時候實例化instance顯然是不合適的。這個時候, 這種方式相比第三和第四種方式就顯得很合理。 對象

第六種(枚舉):

public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
    }
}

這種方式是 Effective Java做者 Josh Bloch 提倡的方式, 它不只能避免多線程同步問題, 並且還能防止反序列化從新建立新的對象, 可謂是很堅強的壁壘啊, 不過, 我的認爲因爲 1.5 中才加入 enum 特性, 用這種方式寫難免讓人感受生疏, 在實際工做中, 我也不多看見有人這麼寫過。

第七種(雙重校驗鎖):

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 = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。

  • 給 singleton 分配內存

  • 調用 Singleton 的構造函數來初始化成員變量,造成實例

  • 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)
    可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance.

在JDK1.5以後, 雙重檢查鎖定纔可以正常達到單例效果,1.5以前有個坑。

說這個坑以前咱們要先來看看volatile這個關鍵字。其實這個關鍵字有兩層語義。第一層語義相信你們都比較熟悉,就是可見性。可見性指的是在一個線程中對該變量的修改會立刻由工做內存(Work Memory)寫回主內存(Main Memory),因此會立刻反應在其它線程的讀取操做中。順便一提,工做內存和主內存能夠近似理解爲實際電腦中的高速緩存和主存,工做內存是線程獨享的,主存是線程共享的。volatile的第二層語義是禁止指令重排序優化。你們知道咱們寫的代碼(尤爲是多線程代碼),因爲編譯器優化,在實際執行的時候可能與咱們編寫的順序不一樣。編譯器只保證程序執行結果與源代碼相同,卻不保證明際指令的順序與源代碼相同。這在單線程看起來沒什麼問題,然而一旦引入多線程,這種亂序就可能致使嚴重問題。volatile關鍵字就能夠從語義上解決這個問題。
可是很不幸,禁止指令重排優化這條語義直到jdk1.5之後才能正確工做。此前的JDK中即便將變量聲明爲volatile也沒法徹底避免重排序所致使的問題。因此,在jdk1.5版本前,雙重檢查鎖形式的單例模式是沒法保證線程安全的。

總結

有兩個問題須要注意:

  1. 若是單例由不一樣的類裝載器裝入, 那便有可能存在多個單例類的實例。假定不是遠端存取, 例如一些servlet容器對每一個servlet使用徹底不一樣的類 裝載器, 這樣的話若是有兩個servlet訪問一個單例類, 它們就都會有各自的實例。

  2. 若是 Singleton 實現了 java.io.Serializable 接口, 那麼這個類的實例就可能被序列化和復原。無論怎樣, 若是你序列化一個單例類的對象, 接下來複原多個那個對象, 那你就會有多個單例類的實例。

對第一個問題修復的辦法:

private static Class getClass(String classname) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        if (classLoader == null) {
            classLoader = Singleton.class.getClassLoader();
        }

        return (classLoader.loadClass(classname));
}

對第二個問題修復的辦法:

public class Singleton implements java.io.Serializable {
    public static Singleton INSTANCE = new Singleton();

    protected Singleton() {
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

對我來講, 我比較喜歡第三種和第五種方式, 簡單易懂, 並且在JVM層實現了線程安全(若是不是多個類加載器環境), 通常的狀況下, 我會使用第三種方式, 只有在要明確實現lazy loading效果時纔會使用第五種方式, 另外, 若是涉及到反序列化建立對象時我會試着使用枚舉的方式來實現單例, 不過, 我一直會保證個人程序是線程安全的, 並且我永遠不會使用第一種和第二種方式, 若是有其餘特殊的需求, 我可能會使用第七種方式, 畢竟, JDK1.5已經沒有雙重檢查鎖定的問題了。
不過通常來講, 第一種不算單例, 第四種和第三種就是一種, 若是算的話, 第五種也能夠分開寫了。因此說, 通常單例都是五種寫法。懶漢, 惡漢, 雙重校驗鎖, 枚舉和靜態內部類。

三大要點

  • 線程安全

  • 延遲加載

  • 序列化與反序列化安全

除了枚舉形式, 其餘實現方式都有兩個共同的缺點

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

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

相關文章
相關標籤/搜索