1. 程式人生 > 其它 >雙重鎖檢查單例、列舉單例、靜態內部類單例

雙重鎖檢查單例、列舉單例、靜態內部類單例

技術標籤:設計模式單例模式

單例最基本要素:

  1. 私有靜態屬性,用於存取類的唯一例項。
  2. 公共靜態方法,用於提供對該唯一例項的存取訪問,如果例項未建立,則建立該例項。
  3. 用於限制類再次例項化的方式。通常使用私有構建函式的方式來實現。

最簡單的單例

public class Singleton {
    private Singleton() {}  //私有建構函式
    private static Singleton instance = null;  //單例物件
    //靜態工廠方法
    public static Singleton getInstance() {
        if (
instance == null) { instance = new Singleton(); } return instance; } }

上述單例執行緒不安全,如在instance未例項化時,就有多個執行緒一起呼叫getInstance()方法,則會創建出多個物件。

雙重加鎖單例實現

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

經過兩個加鎖檢測,看上去是很安全了,但是還是有執行緒安全問題,因為涉及到了JVM編譯器的指令重排。

指令重排是什麼意思呢?

比如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:初始化物件

我們需要在instance物件前面增加一個修飾符volatile。

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關鍵字:

  1. 阻止了變數訪問前後的指令重排,保證了指令執行順序。
  2. 保證執行緒訪問的變數值是主記憶體中的最新值。

用靜態內部類實現單例模式:

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

雙重鎖檢查單例、靜態內部類單例都可以實現執行緒安全的單例模式,但是不能防止用反射的方式打破單例,如下。

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

如何創建出執行緒安全又不能被反射破壞單例性的單例呢?

答案是用列舉實現單例模式。

列舉實現單例模式

/**
 * 最完美的單例模式
 * @author Administrator
 *
 */
public class MySingleton {

	public enum MyEnumSingle{
		INSTANCE;
		private MySingleton singleOne;
		
		private MyEnumSingle(){
			System.out.println("初始化單例");
			singleOne = new MySingleton();
		}
		
		public MySingleton getInstance(){
			return singleOne;
		}
	}
	
	private MySingleton(){}
	
	public static MySingleton getInstance(){
		return MyEnumSingle.INSTANCE.getInstance();
	}
	
}
  1. 列舉模式的單例是餓漢模式
  2. 使用列舉實現的單例模式,不但可以防止利用反射強行構建單例物件,而且可以在列舉類物件被反序列化的時候,保證反序列的返回結果是同一物件。

在這裡插入圖片描述