1. 程式人生 > >單列模式之執行緒安全實現

單列模式之執行緒安全實現

單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、列舉

 

懶漢式執行緒不安全

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

懶漢式,執行緒安全

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

  

雙重檢驗鎖

public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class
) { if (instance == null) { //Double Checked instance = new Singleton(); } } } return instance ; }

 

事實上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 instance 分配記憶體
  2. 呼叫 Singleton 的建構函式來初始化成員變數
  3. 將instance物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。

我們只需要將 instance 變數宣告成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton instance; //宣告成 volatile
    private Singleton (){}
 
    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
   
}

有些人認為使用 volatile 的原因是可見性,也就是可以保證執行緒在本地不會存有 instance 的副本,每次都是去主記憶體中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile 遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。

 

餓漢式

public class Singleton{
    //類載入時就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}
 
    public static Singleton getInstance(){
        return instance;
    }
}

餓漢式的建立方式在一些場景中將無法使用:譬如 Singleton 例項的建立是依賴引數或者配置檔案的,在 getInstance() 之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了

 

 

靜態內部類

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

 

 

列舉 Enum

public enum EasySingleton{
    INSTANCE;
}

我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新建立新的物件。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。

 

就我個人而言,一般情況下直接使用餓漢式就好了,如果明確要求要懶載入(lazy initialization)會傾向於使用靜態內部類,如果涉及到反序列化建立物件時會試著使用列舉的方式來實現單例到反序列化建立物件時會試著使用列舉的方式來實現單例。