設計模式——單例模式

《Head First 設計模式》 學習筆記,碼雲同步更新中

如有錯誤或不足之處,請一定指出,謝謝~

目錄

設計原則
設計模式

單例模式(Singleton Pattern)

  • 定義:
    • 例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。
  • 優點:
    • 節約系統資源,避免頻繁創建銷燬對象帶來的開支
    • 保證對象的全局唯一性
    • 在類的實例化進程上有一定的靈活性
    • 進行擴展之後可以允許存在可變數目的實例
  • 缺點:
    • 擴展困難
    • 一定程度上違反了“單一職責原則”
  • 使用場景:
    • 系統要求有且只有一個對象實例,比如全局唯一序列號生成器,
    • 系統考慮到性能,避免某對象頻繁創建銷燬
    • 使用者除單例提供的單個訪問點外,不允許通過其他途徑訪問實例。

實現單例模式的方法

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

    private Singleton() { // 私有的構造函數,防止用new創建對象
    }

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

在第一次加載類時就初始化實例,當如果客戶端一直沒有使用,會造成性能浪費。也無法依賴參數或配置文件實例化。
簡單,但不可取。

懶漢式(線程不安全)
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

採用了懶加載模式,只有當有人第一次使用該實例時纔會初始化。
但有一個致命問題:線程不安全。當多個線程並行調用getInstance()方法時,會創建多個實例。

懶漢式(線程安全)
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

爲了解決線程安全問題,給getInstance()方法加上synchronized關鍵字。
但這種做法很不高效。因爲實際上只有第一次調用時才需要同步,確保只會創建一個對象。
而這樣做會讓隨後的所有調用都是同步的,也就是同一時間只有一個線程可以訪問,會影響性能。

雙重檢驗鎖
public class Singleton {
    private volatile static Singleton instance; // 注意volatile修飾

    private Singleton() {
    }

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

可以看到,先進性第一次檢驗,通過則進入同步塊。未避免多個線程同時進入同步塊,需要在同步塊內部進行第二次檢驗。
這樣既保證了線程安全,又保證了性能(同步塊只會在第一次實例化的時候執行)
但這裏要注意一個問題,instance變量需要被volatile關鍵字修飾,原因是:
instance = new Singleton();
操作並非是原子性的,實際上JMM做了大概這3件事:

  1. 給instance分配內存
  2. 調用Singleton的工造函數,初始化成員變量
  3. 將instance對象指向分配的內存空間。(在這一步之前instance還是null,執行之後就不再是null了)

而JMM會做重排序優化,也就是實際上的執行順序有可能是1-3-2,而如果當線程A先將3執行完畢,還沒執行2之前,另一個線程就來獲取實例,
(instance已經不是null了,但還沒有初始化),顯然會報錯。

注:此處JMM相關知識可以看我的另外一篇文章:深入理解Java內存模型

現在的做法看起來安全高效了,但別急,還有更好的方法實現。

靜態內部類
public class Singleton {

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

    private Singleton() {
    }

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

當getInstance()第一次被調用時,第一次讀取SingletonHolder.INSTANCE。這時候內部類SingletonHolder會被初始化。
初始化時會初始化靜態域,從而創建Singleton實例,因爲是靜態域,只會初始化一次,並且由JMM保證線程安全。

這種方法線程安全,懶漢式實現,沒有性能缺陷,不依賴JDK版本。

還有更好的。

枚舉
public enum EasySingleton {
    /**
     * 實例
     */
    INSTANCE;

    public void doSomething(){

    }
}

使用時只需要EasySingleton.INSTANCE.doSomething();即可
枚舉默認是線程安全的,還有一個最大的優勢是,可以避免反序列化造成的重新創建對象。
因爲Java規範中規定,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,因此在枚舉類型的序列化和反序列化上做了特殊規定。
保證序列化、反序列化後的實例與序列化前相同。

總結

一般情況直接使用惡漢式即可,如有性能要求,可使用靜態內部類或枚舉。

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