設計模式系列-單例模式

特立獨行的豬手發表於2017-03-10

通常Java實現單例模式有很多種方式,大致可分為懶漢模式餓漢模式,其主要區別是例項延遲載入的問題,當然單例模式往往也關注其他問題,如:執行緒安全等。下面試圖來總結單例模式的這些注意點。

程式碼地址:GitHub
本文首發於:lishuo.me

餓漢模式


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

    public static Singleton getInstance() {
        return instance;
    }
}複製程式碼

餓漢模式在類載入時候就例項化物件,使用時直接呼叫getInstance()方法。這個模式下,是執行緒安全的,在多執行緒併發模式下不會重複例項化物件。
缺點:物件過早的例項化,浪費系統資源。

懶漢模式

public class Singleton {
    private Singleton(){}
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}複製程式碼

這種模式下在類載入時候並沒有例項化物件,而是在呼叫getInstance()方法。之所以使用懶漢模式,是為了避免多早的例項化物件,從而浪費系統資源。
缺點:僅適用於單執行緒,執行緒不安全。

改進1 - 引入synchronized

public class Singleton {
    private Singleton(){}
    private static Singleton instance = null;

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}複製程式碼

之所以引入synchronized修飾getInstance()方法,是為了解決執行緒不安全的問題。利用多執行緒同步機制,讓原先的執行緒不安全迴歸到執行緒安全。但引入synchronized會因為執行緒阻塞、切換會帶一些不必要的開銷,從而降低系統效能。

改進2 - 雙重檢查鎖定

public class Singleton {
    private Singleton(){}
    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {              // A
            synchronized (Singleton.class) {
                if (instance == null) {      // B
                    instance = new Singleton(); // C
                }
            }
        }
        return instance;
    }
}複製程式碼

對比改進1中,可以看到synchronized不再修飾一個方法,而是縮減到修改程式碼塊,因為加鎖同步的話,範圍越小,效能影響最小。

這裡可以注意到修飾變數instance的關鍵字增加了volatile。這裡volatile主要作用是提供記憶體屏障,禁止指令重排序。

現有t1、t2兩個執行緒同時訪問getInstance(),假設t1、t2都執行到A處。由於有同步鎖,只能有個1個執行緒獲得鎖,假如t1擁有該同步鎖,t1執行到C處instace = new Singleton()。將會做如下3步驟:
1.分配記憶體空間
2.初始化
3.將instance指向分配記憶體空間
正常的執行順序應為:1->2->3。執行第3步時,這時候的instance就不再是null了。但由於指令重排序的存在,執行順序有可能變化為:1->3->2。當執行3的時候,instance就不再是null,但初始化工作有可能還沒有做完。這時候如果t2獲取鎖執行的話,就會直接獲取有可能還沒有初始化完成的instance。這樣使用instance會引起程式報錯。當然這也是極端情況下,我嘗試幾次無法捕捉重現,但並不意味著問題不存在。volatile當然還是要加的。

Aif判斷作用主要是防止過多是執行緒執行同步程式碼塊;如果是單例模式的話,這裡同步程式碼塊只會被執行一次。Bif判斷作用主要是防止多執行緒作用下重複例項化,保證執行緒安全。這也被稱為:雙重檢查鎖定

雙重檢查鎖定屬於一種兼顧執行緒安全和效能的實現。

改進3 - 靜態內部類

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

    public static Singleton getInstance() {
        return Holder.instance; // 執行Holder的初始化工作
    }
}複製程式碼

使用靜態內部類也是懶漢模式的一種實現,當呼叫ggetInstance()才會觸發載入靜態內部類,從而初始化獲取instance例項。利用靜態內部類的載入機制來保證執行緒安全。

列舉方式


public enum Singleton {
    INSTANCE;
    Singleton(){}

    public Singleton getInstance() {
        return INSTANCE;
    }

}複製程式碼

用列舉方式實現單例模式,是目前比較推薦的。列舉方式的好處是:1、執行緒安全;2、防止反射出現多個例項;3、防止反序列化出現多個例項。

以上是關於java單例模式的一些總結,如有紕漏,還請指出。

程式碼地址:GitHub
本文首發於:lishuo.me

相關文章