單例模式淺析

  單例模式 html

  單例模式是一種比較常見的模式,看起來很簡單,可是要作到高效安全的使用,其中仍是有不少要點的。參考了Head First及衆多網友的文章,稍微總結一下,以備查看。安全

  單例模式的定義:確保一個類只有一個實例,而且提供一個全局訪問點。多線程

1. 最簡單的單例(餓漢模式),程序一加載就對 instance 變量進行初始化,好處是簡單明瞭,缺點:若是初始化的耗費的資源特別多,而以後這個類可能未被使用到,就會形成浪費。併發

public class Singleton{
    // 單例類靜態引用
    private static Singleton instance = new Singleton();  
    // 私有構造方法
    private Singleton(){}  
    // 靜態方法:全局訪問點
    public static Singleton getInstance(){  
        return instance;  
    }  
}  

 

2. 延遲加載(懶漢模式),做爲改進,咱們能夠在使用到的時候纔去建立單例類。性能

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        // 使用的時候去檢測是否已經初始化
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

  這種方式實現的單例是線程非安全的,假設有線程 A 和 B,同時去獲取 LazySingleton 實例,當 A 線程進入 getInstance(),執行到 if (instance == null) 時,此時判斷結果爲真;以後切換到線程 B 執行,此時也執行到 if (instance == null) ,此時判斷結果爲真,會對 instance 變量進行初始化;此時切換回線程 A ,根據以前的判斷結果,也會對 instance 變量進行初始化,此時就會獲得兩個不同的實例,單例失敗。學習

  爲了實現線程安全,只要在 getInstance() 方法上添加 synchronized 關鍵字便可。優化

 

3. 雙重校驗鎖this

  加上 synchronized 關鍵字,實現了線程安全,但又帶來了性能問題。經過同步方法會讓性能大幅降低。spa

  經過代碼咱們能夠看到,instance 變量在整個程序執行期間只會初始化一次,若是隻爲了這一次初始化,每次獲取單例對象都必須在synchronized這裏進行排隊,實在太損耗性能。爲此咱們能夠以下改進:① instance 變量只會初始化一次,把 synchronized 關鍵字加載此處;② 爲了不每次獲取單例對象都在同步代碼上等待,能夠在同步代碼塊外層再加一次 instance == null 判斷。線程

public class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        // 兩層校驗才能確保單例
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

 

4. volatile 關鍵字

   Java中的指令重排優化是指在不改變原語義(即保證運行結果不變)的狀況下,經過調整指令的執行順序讓程序運行的更快。JVM中並無規定編譯器優化相關的內容,也就是說JVM能夠自由的進行指令重排序的優化。這種優化在單線程中是沒有任何問題的,可是在多線程中就會出現問題。

  建立一個變量須要2個步驟:一個是申請一塊內存,調用構造方法進行初始化操做;另外一個是分配一個指針指向這塊內存。這兩個操做誰在前誰在後呢?JVM規範並無規定。那麼就存在這麼一種狀況,JVM是先開闢出一塊內存,而後把指針指向這塊內存,最後調用構造方法進行初始化。假設線程A開始建立 SingletonClass 的實例,此時線程 B 調用了 getInstance() 方法,首先判斷 instance 是否爲 null。按照咱們上面所說的內存模型,A 已經把 instance 指向了那塊內存,只是尚未調用構造方法,所以 B 檢測到 instance 不爲 null,因而直接把 instance 返回了——問題出現了,儘管instance不爲null,但它並無構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,由於裏面尚未收拾。此時,若是B在A將instance構造完成以前就是用了這個實例,程序就會出現錯誤了!

  在 JDK1.5 及以後版本賦予了volatile關鍵字明確用法。volatile的一個語義是禁止指令重排序優化,也就保證了instance變量被賦值的時候對象已是初始化過的,從而避免了上面說到的問題。因此只須要在 instance 變量前加上 volatile 修飾符便可避免Java指令優化帶來的問題

public class DoubleCheckSingleton {

    private static volatile DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        // 兩層校驗才能確保單例
        if (instance == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

注:volatile 關鍵字用法

  ① 可見性。可見性指的是在一個線程中對該變量的修改會立刻由工做內存(Work Memory)寫回主內存(Main Memory),因此會立刻反應在其它線程的讀取操做中(工做內存是線程獨享的,主存是線程共享的)

  ② 禁止指令重排序優化。

 

5. 靜態內部類

  上述 volatile 關鍵字收到 JDK 版本限制,以下的靜態內部類實現方式是確保了延遲加載和線程安全的。

public class StaticInnerSingleton {

    private static class Holder {
        private static StaticInnerSingleton instance = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getSingleton() {
        return Holder.instance;
    }
}

  能夠把 StaticInnerSingleton 實例放到一個靜態內部類中,這樣就避免了靜態實例在 StaticInnerSingleton 類加載的時候就建立對象,而且因爲靜態內部類只會被加載一次,而類的構造必須是原子性的,非併發的,因此這種寫法也是線程安全的

  在上述代碼中,由於 StaticInnerSingleton 沒有 static 修飾的屬性,所以並不會被初始化。直到調用 getInstance() 的時候,會首先加載 Holder

類,這個類有一個 static 的 StaticInnerSingleton 實例,所以須要調用 StaticInnerSingleton 的構造方法,而後 getInstance() 將把這個內部類的instance 返回給使用者。因爲這個 instance 是 static 的,所以並不會構造屢次。 

 

6. 枚舉式單例

public enum EnumSingleton {

    INSTANCE;
    
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public String getSomething() {
        return "String";
    }
}

  使用枚舉除了線程安全和防止反射調用構造器以外,還提供了自動序列化機制,防止反序列化的時候建立新的對象。所以,《Effective Java》書中推薦使用的方法。

 

參考: 深刻Java單例模式  http://devbean.blog.51cto.com/448512/203501/

    你真的會寫單例模式嗎--Java實現 http://www.importnew.com/18872.html 

    感謝二位做者文章,本文僅做整理學習,侵刪!   

相關文章
相關標籤/搜索