1. 程式人生 > 實用技巧 >單例設計模式

單例設計模式

1.什麼是單例設計模式
所謂的單例設計模式簡單的說就是一個類只能構建一個物件的設計模式。單例有如下特點:
①、單例類只能有一個例項
②、單例類必須自己建立自己的例項
③、單例類必須提供外界獲取這個例項的方法
單例設計模式有以下幾種:餓漢,懶漢,雙重鎖,靜態內部類以及列舉類。

2.餓漢設計模式(天生執行緒安全、呼叫效率高、不能延時載入)

public class Singleton {
    //私有的構造方法
    private Singleton(){}
    
    //私有靜態單例物件
    private static final Singleton instance = new Singleton();
    
    public static Singleton getInstance(){
        return instance;
    }
}

3.懶漢設計模式(呼叫效率不高,能延時載入)
不安全的設計方式:

public class Singleton {
    //私有的構造方法
    private Singleton(){}

    //私有靜態單例物件
    private static Singleton instance = null;

    //獲取單例的方法
    public static Singleton getInstance(){
        if (null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

上述懶漢的寫法不符合執行緒安全的要求,因為當Singleton第一次被初始化的時候,兩個執行緒同時同時訪問getInstance()方法,此時instance == null,兩個執行緒同時通過了判斷,執行了兩個new,這樣instance便被構建了兩次。

舉個栗子:當有三個執行緒同時執行的時候

   public static void main(String[] args) throws Exception {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        };
            new Thread(runnable).start();
            new Thread(runnable).start();
            new Thread(runnable).start();
    }

執行結果:(當然也可能出現一樣的結果,下面這個是極端的情況)
design.Singleton@893a0ba
design.Singleton@213a0ba
design.Singleton@383a0ba
分析:因為三個執行緒同時呼叫了getInstace()方法,執行了instance = null,判斷都為true,各自建立了一個物件.

安全寫法:
學習了多執行緒後可以加 synchronized 關鍵字在方法上實現加鎖同步

public class Singleton {
    //私有的構造方法
    private Singleton(){}

    //私有靜態單例物件
    private static Singleton instance = null;

    //方法同步,呼叫效率低
    public static synchronized Singleton getInstance(){
        if (null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

餓漢和懶漢的區別:
餓漢是主動找食物吃,懶漢是躺著等著食物吃,沒有才主動去找。

4.雙重鎖檢測
上述懶漢設計模式,雖然實現了執行緒安全,但是因為Synchronized將整個方法鎖起來了,每個執行緒必須等其他的執行緒執行完這個方法才有機會呼叫此方法,如果此時方法中存在耗時操作,將會很麻煩。

舉個栗子:

public static synchronized Single getInstance(){
Thread.sleep(10000);  //假如等待10秒,將會出現所有執行緒都要執行10秒才輪到下一個執行緒執行,3執行緒就執行30秒,多執行緒為了提高效率但卻適得其反了這樣。
    if (s == null){
        s = new Single();
    }
    return s;
}

這就引出了雙重鎖設計,Double CheckLock(DCL):

public class Singleton {
    //私有的構造方法
    private Singleton(){} 

    //私有靜態單例物件
    private static Singleton instance = null;    
   
    //獲取單例的方法
    public static Singleton getInstance(){
        if (null == instance){//雙重檢測機制
            synchronized (Singleton.class){//同步鎖
                if (instance == null){}//雙重檢測
                instance = new Singleton();
            }
        }
        return instance;
    }
}

仔細看上面的程式碼,乍一看,沒有什麼問題了,但是仔細瞧,這裡並非絕對的執行緒安全。
分析:
Java中instance = new Singleton會被編譯器編譯成如下JVM編譯器指令
memory =allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance =memory; //3:設定instance指向剛分配的記憶體地址
但是這些指令順序並非一成不變,有可能會經過JVM和CPU的優化,指令重排成下面的順序:
memory =allocate(); //1:分配物件的記憶體空間
instance =memory; //3:設定instance指向剛分配的記憶體地址
ctorInstance(memory); //2:初始化物件
當執行緒A執行完1、3的時候,instance物件還未完成初始化,此時instance物件已經不再是null了,此時執行緒B搶佔到CPU資源,執行if(instance == null)的結果會是false,從而直接返回了一個沒有初始化完成的instance物件。

優化:

public class Singleton {
    private Singleton() {}  //私有建構函式
    private volatile static Singleton instance = null;  //單例物件
    //靜態工廠方法
    public static Singleton getInstance() {
          if (instance == null) {      //雙重檢測機制
          synchronized (Singleton.class){  //同步鎖
           if (instance == null) {     //雙重檢測機制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

Volatile:保證了變數在記憶體的可見性,即一個變數被volatile修飾了,那麼它的改變對於各個執行緒都是可見的。它還阻止優化的編譯器優化後續的讀或寫操作,從而錯誤地重用陳舊的值或忽略寫操作
簡單的來說就是保證了下述123的順序,避免了返回未初始化完成的instance
memory =allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance =memory; //3:設定instance指向剛分配的記憶體地址

5.靜態內部類(執行緒安全,呼叫效率高,可以延時載入)

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton() {
    }      //私有的構造方法

    /**
     * 獲取單例物件的方法
     *
     * @return Singleton
     */
    public Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

說明:
1.從外部無法訪問靜態內部類LazyHolder,只有當呼叫Singleton.getInstance方法的時候,才能得到單例物件INSTANCE。
2.INSTANCE物件初始化的時機並不是在單例類Singleton被載入的時候,而是在呼叫getInstance方法,使得靜態內部類LazyHolder被載入的時候。因此這種實現方式是利用classloader的載入機制來實現懶載入,並保證構建單例的執行緒安全。

6.列舉類(執行緒安全,呼叫效率高,不能延時載入,可以天然防止反射和序列化的呼叫)
靜態內部類的方法雖然很好,但是存在著單例共同的問題:無法防止利用反射來重複構建物件。

舉個栗子:

//獲得構造器
Constructor con = Singleton.class.getDeclaredConstructor();
//設定為可訪問
con.setAccessible(true);
//構造兩個不同的物件
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//驗證是否是不同物件
System.out.println(singleton1.equals(singleton2));

列印:false
很顯然,兩個例項不一樣

列舉:本身是個類,且是靜態類,就是一個單例的,預設被final修改類,不能被繼承,列舉中只有ToString沒有被final修飾,列舉是自己內部例項化物件,這種其實也是一種餓漢式
優點:程式碼簡單,防止序列化

public enum Singleton {
    INSTANCE;

    public void doSomeThing() {
        System.out.println("what do you what to do.");
    }
}
class Test{
	  public static void main(String[] args) throws Exception {
        Singleton.INSTANCE.doSomeThing();
    }
}