1. 程式人生 > >淺談單例(java實現)

淺談單例(java實現)

前言:

       在軟體開發中,各個模組之間如何組織能使我們的系統更加優雅健壯,來保證程式碼具有良好的複用性,擴充套件性以及可讀性,這對軟體設計人員至關重要。於是經過前人反覆推敲,論證,使用之後被大多數人知曉,理解形成一套設計模式體系(一種套路,或者武功祕籍(比如葵花寶典、九陰真經)),並由Erich GammaRichard HelmRalph Johnson John Vlissides 合著(1995年,簡稱‘四人幫’書或GOF23設計模式),今天介紹裡面一種常用的設計模式——單例模式。

定義:

        在java應用中,單例模式能夠保證一個類只有一個物件例項,並提供一個訪問該例項的全域性訪問點。

應用場景:

       1.比如Windows系統的工作管理員,不管開啟多少次工作管理員,只會彈出一個視窗。如果不使用單例機制,將彈出多個視窗,如果這些視窗顯示的內容完全一致,則是重複物件,浪費記憶體資源;如果這些視窗顯示的內容不一致,則意味著在某一瞬間系統有多個狀態,與實際不符。

        2.還有windows的回收站,在整個系統中,回收站一直維護著僅有的一個例項。

        3.再比如我們的檔案系統,一個作業系統只有一個檔案系統。

實現思想:

       構造器私有化(無法通過構造器來建立物件,只能通過靜態方法獲取例項物件),自身維護一個例項物件(static修飾,也是私有的,確保整個系統只有一個例項),提供一個獲取例項的靜態方法(提供全域性訪問點)。

實現方式:

       單例模式的幾種實現方式(由於反射和反序列化可以破壞單例模式,這裡先不考慮,)。

 一、餓漢式單例模式(可用)

public class Singleton{
    //類載入的時候初始化物件,天然的執行緒安全,不能延時載入
    private static Singleton instance = new Singleton();
    //構造器私有
    private Singleton(){
    }
    //通過靜態方法獲取例項物件
    public static Singleton getInstance(){
        return instance;
    }
}

       特點:執行緒安全(由於類載入機制,JVM能夠保證在載入類的時候免疫許多由多執行緒引起的問題,所以是天然安全的),獲取例項不用考慮加鎖,對應的就效率高,但是不能延時載入,不管用不用這個物件,都會例項化(可能會造成資源浪費)。

二、懶漢式單例模式(可用)

​​​​public class Singleton{
    //類載入的時候不初始化物件,什麼時候用什麼時候初始化
    private static Singleton instance;
    private Singleton(){
    }
    //加同步鎖保證執行緒安全,呼叫效率低,真正用的時候才建立物件,延時載入
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

        特點:執行緒安全(必須加鎖,不然當instance==null的時候兩個執行緒可以併發的進入if語句從而會建立多個物件,違反了單例初衷),執行效率低,可以延時載入,真正用的時候再建立物件(資源利用率高)。

三、雙重檢測鎖(推薦)

public class Singleton{
    //加上volatile關鍵字禁止java記憶體模型的指令重排序機制
    private static volatile Singleton instance = null;
    private Singleton(){

    }
    //只對需要鎖的部分程式碼加鎖
    public static Singleton getInstance(){
        if(instance == null){
            //只需在第一次建立物件的時候加同步塊,執行效率高
            ​​synchronized(Singleton.class)
            //雙重判斷,併發情況下保證只有一個例項
            if(instance == null){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

       特點:執行緒安全(雙重檢測判斷),效率高(因為這裡只需要在第一次建立物件時同步,一旦建立成功,以後獲取例項的時候就不需要再同步了),可以延時載入(顯而易見,在真正呼叫的時候才建立物件)

Volatile關鍵字語義:

       當一個變數定義為volatile之後,它將具備兩種特性,第一是保證此變數對所有執行緒的可見性;第二是禁止指令重排序優化。

雙重檢測鎖在jdk1.5之前理論上的實現想法是完美的,但是實際上是行不通的,這是由於java的記憶體模型有一個指令重排序機制,可能會導致一個已存在卻不完整的instance例項物件。JVM在建立物件時是一個非原子性操作,通過new關鍵字建立物件可以分為三個步驟:

1.為instance分配記憶體空間;

2.利用構造器初始化物件;

3.將instance引用指向剛分配的記憶體地址(執行完這步instance就為非空了)。

這個過程可能發生指令重排序,也就是說上面三個步驟可能會打亂順序,但不是說指令任意重排序,CPU需要正確處理指令依賴情況以保證程式能得出正確的執行結果,在當前情況下,指令2依賴於指令1,所以1,2的順序不可能變,但是指令3並不依賴於指令2,所以可能會出現這樣一種情況,執行1之後,再執行3,最後才執行2。這種情況不僅是可能的,而是有一些JIT編譯器真實發生的現象。瞭解了指令重排機制之後,我們再回頭看上面程式碼,如果沒有volatile關鍵字,當執行緒A執行new Singleton()建立物件,並且將instance引用指向這個物件在記憶體的地址(這時instance非空),但是Singleton建構函式並沒有執行,也就是說步驟1,3已經執行完畢,步驟2還沒有執行,同時執行緒A被執行緒B佔領,此時B得到的會是一個不完整的物件(未被初始化的物件),判斷不為空,直接return instance,從而導致系統崩潰。

       Volatile遮蔽指令重排序的語義是在jdk1.5的時候才完全修復,此前即使將變數定義為volatile也不能完全避免重排序所導致的問題,這點也是在jdk1.5之前的java中無法安全的使用雙重檢測鎖來實現單例模式的原因。

四、靜態內部類實現單例(推薦)

public class Singleton{
    //載入Singleton類的時候不會載入靜態內部類,可以實現延時載入
    private static class Inner{
        //靜態內部類維護一個例項物件
        private static Singleton instance = new Singleton();
    }
    private Singleton(){

    }
    //類載入的時候天然執行緒安全,不用同步鎖,呼叫效率高
    public static Singleton getInstance(){
        //呼叫的時候才會載入內部類
        return Inner.instance;
    }
}

       特點:延時載入(載入外部類的時候並不會載入內部類,真正呼叫getInstance()的時候才會載入),執行緒安全(同樣類載入的時候天然執行緒安全,所以不用同步,執行效率高)。

通過程式碼測試載入外部類的時候靜態內部類是否會被載入

public class Singleton{
    //載入Singleton類的時候不會載入靜態內部類,可以實現延時載入
    private static class Inner{
        //靜態內部類維護一個例項物件
        private static Singleton instance = new Singleton();
            
        static{
            System.out.println("內部類已被載入");
        }
    }
    private Singleton(){

    }
    //類載入的時候天然執行緒安全,不用同步鎖,呼叫效率高
    public static Singleton getInstance(){
        //呼叫的時候才會載入內部類
        return Inner.instance;
    }
    
    static{
        System.out.println("外部類已被載入");
    }
}

在外部類和內部類都加上一個靜態程式碼塊,我們知道載入類的時候static塊程式碼也會載入,寫個測試類看下控制檯結果

public class Test{

    ​​​​​​public static void main(String[] args) {
		try{
			Class clazz = Class.forName("com.ahua.singleton.Singleton");
		}catch (ClassNotFoundException e) {
		    e.printStackTrace();
		}
	}
}

這裡利用反射載入了Singleton類,外部內的靜態塊已執行,很明顯內部類是不會被載入的。

五、列舉實現單例(推薦)

public enum Singleton{
    //宣告一個列舉物件,列舉本省就是單例
    INATNCE;
}

        特點:簡潔(利用列舉獨特機制可以很簡單的實現單例),執行緒安全(列舉底層是由static修飾的,靜態資源初始化過程也是天然安全的),可以防止反射(閱讀Constructor的newInstance()方法可以看到反射呼叫構造器建立物件的時候會先判斷是否為列舉物件,如果是,就會丟擲異常(“cannot reflectivy create enum object”),反射失敗)和反序列化(列舉對序列化處理可以參考http://www.hollischuang.com/archives/197這篇文章)來破壞單例機制。不能延時載入(靜態資源在類載入的時候自動載入)

總結:

通過以上幾種實現方式,我們可以知道在運用單例模式往往要考慮以下幾個效能:

  1. 是否能夠延時載入,充分利用資源
  2. 是否執行緒安全
  3. 併發情況下的訪問效能
  4. 是否可以防止反射和反序列化漏洞

參考資料:

       《深入理解JVM》

       《大話設計模式》