單例模式

GitHub代碼

場景描述:假設我們需要一個全局唯一的實例,比如線程池,緩存等。

初步解決:比如所有開發人員通過約定,或者全局變量來實現。
問題:將對象賦值給一個全局變量,但並不一定會立即使用它,從而佔用內存資源,造成資源的浪費。

更好的方案:使用單例模式。

經典的單例模式代碼如下:

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton(){}

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

單件模式確保一個類只有一個實例,並提供一個全局訪問點。

這種做法對資源敏感的對象特別重要。

相應的類圖:
在這裏插入圖片描述
新的問題:假設我們的程序是一個多線程的程序,如果使用上述經典代碼就會出現多個單例對象的奇特情況。因爲我們缺少了同步。

第一種解決方案:將方法變爲同步方法

public class Singleton_One {
    private static Singleton_One uniqueInstance;

    private Singleton_One(){}

    private static synchronized Singleton_One getInstance(){
        if (uniqueInstance == null){
            uniqueInstance = new Singleton_One();
        }
        return uniqueInstance;
    }
}

這種方法帶來的缺陷是每次訪問方法都會進行同步,造成性能的下降。而我們實際上只需要在第一次進行同步即可。

第二種方法,不使用延遲實例化,而是急切的創建實例

public class Singleton_Two {
    private static Singleton_Two uniqueInstance = new Singleton_Two();

    private Singleton_Two(){}

    public static Singleton_Two getInstance(){
        return uniqueInstance;
    }
}

這種方法的缺陷類似於使用全局變量的問題,雖然提前創建了實例,但我們並不能保證創建了後就立即使用,也會造成一定程度的浪費。

第三種方法雙重檢查加鎖,曾經的一種經典做法。

public class Singleton_Three {
    private volatile static Singleton_Three uniqueInstance;

    private Singleton_Three(){}

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

只有在第一次進行同步,並且使用了volatile變量避免了重排序造成的問題,但一方面由於其自身的缺陷,另一方面同步性能問題的提升,導致這種解決方案已經變爲歷史,不推薦使用,複雜且難以理解。

第四種方法延遲初始化佔位類

public class Singleton_Four {
    private static class ResourceHolder{
        public static Resource resource = new Resource();
    }

    public static Resource getResource(){
        return ResourceHolder.resource;
    }
}
public class Resource {
    // 一個資源類,需要被單例加載
}

當任何線程第一次調用getResource時,都會使ResourceHolder被加載和被初始化,此時靜態初始化器將執行Resource的初始化操作。這種方法在第二種提前實例化的方法上改進而來,並且不需要同步。因此最推薦在多線程情況下使用該方法。

注意:每個類加載器都定義了一個命名空間,如果有兩個以上的類加載器,不同的類加載器可能會加載同一個類,從整個程序來看,同一個類會被加載多次。如果這樣的事情發生在單例上,就會產生多個單例並存的怪異現象。所以如果你的程序有多個類加載器又同時使用了單例模式,請小心。

總結:當你的程序中迫切需要在全局只需一個實例對象的情況時,考慮使用單例模式,並推薦使用延遲初始化佔位類方式避免多線程問題。但過度使用單例模式有可能適得其反,所以在設計時需要慎重考慮。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章