Java單例模式8種方式 詳解

Singleton

所謂單例,指的就是單實例,有且僅有一個類實例。
運用場景很多,例如網站的在線人數,window系統的任務管理器,網站計數器等等,這些都是單例模式的運用。單例模式有常見的8種形式,如下:

1.Lazy1【不可用】

  • 懶漢式1:

    • 線程不穩定

    • 延遲初始化

    • 多線程不安全

    • 是最基本的實現方式,不支持多線程,因爲沒有synchronized加鎖,多線程不能工作。

    • 實現圖

    圖一

    • 多線程則會出現,當Singleton_Lazy1類剛剛被初始化,instance對象還是空,這時候兩個線程同時訪問到getInstance方法,因爲Instance是空,所以A\B兩個線程都通過了instance爲空的判斷,則A\B兩個線程都會實例化對象,單例失敗。

      不加同步的懶漢式是線程不安全的,如下示例:

在這裏插入圖片描述

2.Lazy2(同步方法)【不建議使用】

  • 懶漢式2:

    • 線程穩定

    • 延遲初始化

    • 多線程安全

    • 優點:調用才初始化,避免內存浪費。

    • 缺點:必須加鎖synchronized才能保證單例,但每次調用都要加鎖會影響效率。

    • 實現圖
      在這裏插入圖片描述

    • 利用synchronized關鍵字對getInstance方法加鎖使得多線程安全且穩定但效率不高。

      多線程實現方法,如下圖所示:

    在這裏插入圖片描述

3.DCL1(同步代碼塊)【不可用】

  • 雙重檢測機制:

    • 延遲初始化

    • 多線程不安全

    • 線程穩定

    • 1.爲了防止new Singleton被執行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類(注意,這裏不能使用對象鎖)。

    • 2.進入Synchronized 臨界區以後,還要再做一次判空。因爲當兩個線程同時訪問的時候,線程A構建完對象,線程B也已經通過了最初的判空驗證,不做第二次判空的話,線程B還是會再次構建instance對象。這種同步並不能起到線程同步的作用

       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編譯器的指令重排。

      一般創建一個對象的時候會有三個步驟:

      instance = new Singleton
      Value =allocate();    //1:分配對象的內存空間 
      ctorInstance(Value);  //2:初始化對象 
      instance =Value;     //3:設置instance指向剛分配的內存地址 
      

      但這三步並不是固定不變的,有可能會經過JVM和CPU的優化,指令將會重排爲:

      instance = new Singleton
      Value =allocate();  //1:分配對象的內存空間 
      instance =Value;   //3:設置instance指向剛分配的內存地址 
      ctorInstance(Value); //2:初始化對象 
      

      當線程A執行完1、3時,instance對象還未完成初始化,但是已經不知向null。這個時候如果B線程進入CPU,則會搶先執行if(instance == null)的判斷結果爲false,這個時候getInstance方法則會返回一個沒有初始化完成的instance對象,結果如下圖:

      在這裏插入圖片描述

4.DCLPro【推薦使用】

  • 雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking):
    • 線程穩定

    • 延遲初始化

    • 多線程安全

    • volatile關鍵字是防止創建對象時的重排序,在訪問volatile變量時不會執行加鎖操作。

      volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內存中的最新值。

    • 完美解決3.Lazy3的問題。

5.Hunger1(靜態常量)【可用】

  • 餓漢式(靜態常量):
    • 線程穩定

    • 不會延遲加載

    • 多線程安全

    • 優點:採用了類裝載的機制來保證初始化實例時只有一個線程,避免了線程同步問題。沒有用synchronized進行加鎖,提高執行效率。

    • 缺點:在類裝載的時候就完成實例化,沒有達到延遲加載的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

      image-20210115210718610

6.Hunger2(靜態代碼塊)【可用】

  • 餓漢式(靜態代碼塊):
    • 線程穩定

    • 不會延遲初始化

    • 多線程安全

    • 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。沒有用synchronized進行加鎖,提高執行效率。

    • 缺點:在類裝載的時候就完成實例化,沒有達到延遲加載的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

    • 這種方式和上面的方式其實類似,只不過將類實例化的過程放在了靜態代碼塊中,也是在類裝載的時候,就執行靜態代碼塊中的代碼,初始化類的實例。

      static{
             
             
         instance = new Singleton_Hunger2;
      }
      

7.Pattern(靜態內部類)【推薦使用】

  • 靜態內部類:
    • 線程穩定

    • 延遲加載

    • 多線程安全

    • 優點:和5.Hunger1類似,採用了類裝載的機制來保證初始化實例時只有一個線程。
      但靜態內部類則可以達到延遲加載的效果,在Singleton_Pattern類被裝載的時候並不會馬上實例化,而是在需要實例化的時候再調用getInstance方法實例化,這樣纔會裝載靜態內部類SingletonInstance,從而達到Singleton_Pattern的實例化。 類的靜態屬性只會在第一次加載類的時候初始化,如此在類初始化的時候其他進程是無法進入的,從而保護了線程的安全。

    • 總結爲:避免了線程不安全,延遲加載,效率高。

      在這裏插入圖片描述

8.Enum【推薦使用】

不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。代碼簡潔。使用起來方便。

9.總結

1.Singleton_Lazy1:

public class Singleton_Lazy1 {
   
   
	private Singleton_Lazy1() {
   
   
	};

	private static Singleton_Lazy1 instance = null;

	public static Singleton_Lazy1 getInstance() {
   
   
		if (instance == null) {
   
   
			instance = new Singleton_Lazy1();
		}
		return instance;
	}
}

2.Singleton_Lazy2:

public class Singleton_Lazy2 {
   
   
	private Singleton_Lazy2() {
   
   
	};

	private static Singleton_Lazy2 instance = null;

	public static synchronized Singleton_Lazy2 getInstance() {
   
   
		if (instance == null) {
   
   
			instance = new Singleton_Lazy2();
		}
		return instance;
	}
}

3.Singleton_DCL1:

public class Singleton_DCL1 {
   
   
	private Singleton_DCL1() {
   
   
	};

	private static Singleton_DCL1 singleton;

	public static Singleton_DCL1 getInstance() {
   
   
		if (singleton == null) {
   
   
			synchronized (Singleton_DCL1.class) {
   
   
					singleton = new Singleton_DCL1();
			}
		}
		return singleton;
	}
}

4.Singleton_DCLPro:

public class Singleton_DCLPro {
   
   
	private Singleton_DCLPro() {
   
   
	};

	private volatile static Singleton_DCLPro singleton;

	public static Singleton_DCLPro getInstance() {
   
   
		if (singleton == null) {
   
   
			synchronized (Singleton_DCLPro.class) {
   
   
				if (singleton == null) {
   
   
					singleton = new Singleton_DCLPro();
				}
			}
		}
		return singleton;
	}
}

5.Singleton_Hunger1:

public class Singleton_Hunger1 {
   
   
	private final static Singleton_Hunger1 INSTANCE = new Singleton_Hunger1();

	private Singleton_Hunger1() {
   
   
	};

	public static Singleton_Hunger1 getInstance() {
   
   
		return INSTANCE;
	}
}

6.Singleton_Hunger2:

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

	private Singleton_Hunger2() {
   
   
	};

	public Singleton_Hunger2 getInstance() {
   
   
		return instance;
	}
}

7.Singleton_Pattern:

public class Singleton_Pattern {
   
   
	private Singleton_Pattern() {
   
   
	};

	private static class SingletonInstance {
   
   
		private static final Singleton_Pattern INSTANCE = new Singleton_Pattern();
	}

	public Singleton_Pattern getSingleton() {
   
   
		return SingletonInstance.INSTANCE;
	}
}

8.Singleton_E:

public enum Singleton_E {
   
   
	INSTANCE;
	public void whateverMethod() {
   
   
		System.out.println("這是一個枚舉單例");
	}
}

關鍵字

synchronized

synchronized的使用

  • 修飾實例方法,對當前實例對象加鎖
  • 修飾靜態方法,多當前類的Class對象加鎖
  • 修飾代碼塊,對synchronized括號內的對象加鎖

volatile

在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

volatile關鍵字是防止創建對象時的重排序,在訪問volatile變量時不會執行加鎖操作。

volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內存中的最新值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章