1. 程式人生 > >設計模式之一----單例模式

設計模式之一----單例模式

在一個系統裡,對於某些物件,有且只能有一個,當我們有這樣的需求的時候,就需要用到單例模式了。

單例模式分為懶漢模式和餓漢模式。我們使用程式碼來進行說明:

首先建立一個用到了餓漢模式的Singleton類在com.single下:

package com.single;
/*
 * 單例模式
 * 場合:保證有些物件有且只有一個
 * 型別:餓漢模式
 * */
public class Singleton {
    //1、將構造方法私有化 不允許外部直接建立物件
    private Singleton(){}
    
    //2、類的唯一例項 只要類建立 該例項就會被建立
    private static Singleton instance = new Singleton();
    
    //3、提供一個用於獲取例項的方法
    public static Singleton getInstance(){
        return instance;//類一被建立 該例項就會被建立
    }
}

然後建立一個用到了懶漢模式的Singleton2在com.single下:
package com.single;

public class Singleton2 {
    //1、將構造方法私有化
    private Singleton2(){}
    //2、宣告類的唯一例項
    private static Singleton2 instance;//類載入的時候沒有生成這個例項 而是使用者獲取來才生成這個例項
    //3、提供一個用於獲取例項的方法
    public static Singleton2 getInstance(){
        if(instance == null){//僅當第一次嘗試獲取例項的時候才生成例項
            instance = new Singleton2();
        }
        return instance;
    }
}
最後寫一個測試類Test在com.single下:
package com.single;

public class Test {
    public static void main(String[] args) {
        //餓漢模式
        Singleton s1 = Singleton.getInstance();//獲取例項
        Singleton s2 = Singleton.getInstance();//獲取例項
        System.out.println(s1==s1);//true 同一個例項 無論獲取多少次都是同一個物件
        
        //懶漢模式
        Singleton2 s3 = Singleton2.getInstance();
        Singleton2 s4 = Singleton2.getInstance();
        System.out.println(s3==s4);//true 也是同一個例項
        
        //懶漢模式與餓漢模式的區別:
        //餓漢模式載入類的速度比較慢,但是允許時獲取物件速度比較快 執行緒安全 
        //懶漢模式載入類比較快 但是執行時獲取物件的速度比較慢 執行緒不安全
    }
}

前面我們提到了懶漢模式是執行緒不安全的,所以我們能不能想辦法做成執行緒安全的呢? 來嘗試一下:

package com.single;

public class Singleton2 {
    private static Object lock = new Object();//生成一把鎖
    //1、將構造方法私有化
    private Singleton2(){}
    //2、宣告類的唯一例項
    private static Singleton2 instance;//類載入的時候沒有生成這個例項 而是使用者獲取來才生成這個例項
    //3、提供一個用於獲取例項的方法
    public static Singleton2 getInstance(){
    synchronized(lock){//加上鎖
        if(instance == null){//僅當第一次嘗試獲取例項的時候才生成例項
            instance = new Singleton2();
        }
    }
        return instance;
    }
}

這樣做是不是彷彿沒什麼問題了,但是我們要知道實際上加鎖是開銷特別大的,除了第一次生命單例以外我們用不到每次都加鎖,所以我們採用雙重校驗鎖:

package com.singleton;

public class Singleton2 {
    private Singleton2(){};
    private static Singleton2 instance;
    private static Object lock = new Object();
    public static Singleton2 getInstance(){
        if(instance == null){//只有第一次建立例項才加鎖
            synchronized(lock){//加鎖實際上很浪費時間
                if(instance == null){
                    instance = new Singleton2();
                    System.out.println("建立成功");
                }
            }
        }
        return instance;
    }
    public void save(){
        System.out.println("儲存成功");
    }
}


彷彿沒什麼問題了是吧,但是由於JVM指令重排優化特性的存在,導致初始化Singleton和將物件地址賦給instance欄位的順序可能是不確定的,在某個執行緒建立單例物件時,在構造方法被呼叫之前,就為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance,取到的就是狀態不正確的物件,程式就會出錯。然而volatile關鍵字除了保障變數被多執行緒訪問的安全性的同時,還可以禁止JVM重排優化,所以這個地方最好在instance前加上volatile關鍵字:

package com.singleton;

public class Singleton2 {
    private Singleton2(){};
    private static volatile Singleton2 instance;//懶漢模式 預設執行緒不安全 慢
    private static Object lock = new Object();
    public static Singleton2 getInstance(){
        if(instance == null){
            synchronized(lock){//加鎖實際上很浪費時間
                if(instance == null){
                    instance = new Singleton2();
                    System.out.println("建立成功");
                }
            }
        }
        return instance;
    }
    public void save(){
        System.out.println("儲存成功");
    }
}


然而現在在不需要繼承這個單例物件對應的類的時候有一種最簡單,也絕對執行緒安全的方式:單列舉,詳情點選連結