1. 程式人生 > 程式設計 >Java單例模式的8種寫法(推薦)

Java單例模式的8種寫法(推薦)

單例:Singleton,是指僅僅被例項化一次的類。

餓漢單例設計模式

一、餓漢設計模式

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

 private SingletonHungry() {
 }

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

因為單例物件一開始就初始化了,不會出現執行緒安全的問題。

PS:因為我們只需要初始化1次,所以給INSTANCE加了final

關鍵字,表明初始化1次後不再允許初始化。

懶漢單例設計模式

二、簡單懶漢設計模式

由於餓漢模式一開始就初始化好了,但如果一直沒有被使用到的話,是會浪費珍貴的記憶體資源的,所以引出了懶漢模式。

懶漢:首次使用時才會去例項化物件。

public class SingletonLazy1 {
 private static SingletonLazy1 instance;

 private SingletonLazy1() {
 }

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

測試:

public class Main {
 public static void main(String[] args) {
 SingletonLazy1 instance1 = SingletonLazy1.getInstance();
 SingletonLazy1 instance2 = SingletonLazy1.getInstance();
 System.out.println(instance1);
 System.out.println(instance2);
 }
}

測試結果:從結果可以看出,打印出來的兩個例項物件地址是一樣的,所以認為是隻建立了一個物件。

在這裡插入圖片描述

三、進階

1:解決多執行緒併發問題

上述程式碼存在的問題:在多執行緒環境下,不能保證只建立一個例項,我們進行問題的重現:

public class Main {
 public static void main(String[] args) {
 new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start();
 }
}

結果:獲取到的物件不一樣,這並不是我們的預期結果。

在這裡插入圖片描述

解決方案:

public class SingletonLazy2 {
 private static SingletonLazy2 instance;

 private SingletonLazy2() {
 }
 //在方法加synchronized修飾符
 public static synchronized SingletonLazy2 getInstance() {
 if (instance == null) {
  instance = new SingletonLazy2();
 }
 return instance;
 }
}

測試:

public class Main2 {
 public static void main(String[] args) {
 new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start();
 }
}

在這裡插入圖片描述

結果:多執行緒環境下獲取到的是同個物件。

四、進階2:縮小方法鎖粒度

上一方案雖然解決了多執行緒問題,但由於synchronized關鍵字是加在方法上的,鎖粒度很大,當有上萬甚至更多的執行緒同時訪問時,都被攔在了方法外,大大降低了程式效能,所以我們要適當縮小鎖粒度,控制鎖的範圍在程式碼塊上。

public class SingletonLazy3 {
 private static SingletonLazy3 instance;

 private SingletonLazy3() {
 }
 
 public static SingletonLazy3 getInstance() {
 //程式碼塊1:不要在if外加鎖,不然和鎖方法沒什麼區別
 if (instance == null) {
  //程式碼塊2:加鎖,將方法鎖改為鎖程式碼塊
  synchronized (SingletonLazy3.class) {
  //程式碼塊3
  instance = new SingletonLazy3();
  }
 }
 return instance;
 }
}

測試:

public class Main3 {
 public static void main(String[] args) {
 new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start();
 }
}

我們看一下執行結果:還是出現了執行緒安全的問題(每次執行都可能列印不同的地址情況,只要證明是非執行緒安全的即可)。

在這裡插入圖片描述

原因分析:當執行緒A拿到鎖進入到程式碼塊3並且還沒有建立完例項時,執行緒B是有機會到達程式碼塊2的,此時執行緒C和D可能在程式碼塊1,當執行緒A執行完之後釋放鎖並返回物件1,執行緒B進入進入程式碼塊3,又建立了新的物件2覆蓋物件1並返回,最後當執行緒C和D在進行判null時發現instance非空,直接返回最後建立的物件2。

五、進階3:雙重檢查鎖DCL(Double-Checked-Locking)

所謂雙重檢查鎖,就是線上程獲取到鎖之後再對例項進行第2次判空檢查,判斷是不是有上一個執行緒已經進行了例項化,有的話直接返回即可,否則進行例項初始化。

public class SingletonLazy4DCL {
 private static SingletonLazy4DCL instance;

 private SingletonLazy4DCL() {
 }

 public static SingletonLazy4DCL getInstance() {
 //程式碼塊1:第一次判空檢查
 if (instance == null) {
  //程式碼塊2:加鎖,將方法鎖改為鎖程式碼塊
  synchronized (SingletonLazy3.class) {
  //程式碼塊3:進行第二次(雙重)判空檢查
  if (instance == null) {
   instance = new SingletonLazy4DCL();
  }
  }
 }
 return instance;
 }
}

測試:

public class Main4DCL {
 public static void main(String[] args) {
 new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start();
 }
}

在這裡插入圖片描述

六、進階4:禁止指令重排

在物件的例項過程中,大概可分為以下3個步驟:

  1. 分配物件記憶體空間
  2. 在空間中建立物件
  3. 例項指向分配到的記憶體空間地址

由於例項化物件的過程不是原子性的,且JVM本身對Java程式碼指令有重排的操作,可能1-2-3的操作被重新排序成了1-3-2,這樣就會導致在3執行完之後還沒來得及建立物件時,其他執行緒先讀取到了未初始化的物件instance並提前返回,在使用的時候會出現NPE空指標異常。

解決:給instance加volatile關鍵字表明禁止指令重排,出現的概率不大, 但這是更安全的一種做法。

public class SingletonLazy5Volatile {
 //加volatile關鍵字
 private volatile static SingletonLazy5Volatile instance;

 private SingletonLazy5Volatile() {
 }

 public static SingletonLazy5Volatile getInstance() {
 //程式碼塊1
 if (instance == null) {
  //程式碼塊2:加鎖,將方法鎖改為鎖程式碼塊
  synchronized (SingletonLazy3.class) {
  //程式碼塊3
  if (instance == null) {
   instance = new SingletonLazy5Volatile();
  }
  }
 }
 return instance;
 }
}

七、進階5:靜態內部類

我們還可以使用靜態類的靜態變數被第一次訪問時才會進行初始化的特性來進行懶載入初始化。把外部類的單例物件放到靜態內部類的靜態成員變數裡進行初始化。

public class SingletonLazy6InnerStaticClass {
 private SingletonLazy6InnerStaticClass() {
 }

 public static SingletonLazy6InnerStaticClass getInstance() {
 return SingletonLazy6InnerStaticClass.InnerStaticClass.instance;
 //或者寫成return InnerStaticClass.instance;
 }

 private static class InnerStaticClass {
 private static final SingletonLazy6InnerStaticClass instance = new SingletonLazy6InnerStaticClass();
 }
}

雖然靜態內部類裡的寫法和餓漢模式很像,但它卻不是在外部類載入時就初始化了,而是在第一次被訪問到時才會進行初始化的操作(即getInstance方法被呼叫時),也就起到了懶載入的效果,並且它可以保證執行緒安全。

測試:

public class Main6InnerStatic {
 public static void main(String[] args) {
 new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
 new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start();
 }
}

在這裡插入圖片描述

反射攻擊

雖然我們一開始都對構造器進行了私有化處理,但Java本身的反射機制卻還是可以將private訪問許可權改為可訪問,依舊可以創建出新的例項物件,這裡以餓漢模式舉例說明:

public class MainReflectAttack {
 public static void main(String[] args) {
 try {
  SingletonHungry normal1 = SingletonHungry.getInstance();
  SingletonHungry normal2 = SingletonHungry.getInstance();
  //開始反射建立例項
  Constructor<SingletonHungry> reflect = SingletonHungry.class.getDeclaredConstructor(null);
  reflect.setAccessible(true);
  SingletonHungry attack = reflect.newInstance();
  
  System.out.println("正常靜態方法呼叫獲取到的物件:");
  System.out.println(normal1);
  System.out.println(normal2);
  System.out.println("反射獲取到的物件:");
  System.out.println(attack);
 } catch (Exception e) {
  e.printStackTrace();
 }
 }
}

在這裡插入圖片描述

八、列舉單例(推薦使用)

public enum SingletonEnum {
 INSTANCE;
}

列舉是最簡潔、執行緒安全、不會被反射建立例項的單例實現,《Effective Java》中也表明了這種寫法是最佳的單例實現模式。

單元素的列舉型別經常成為實現Singleton的最佳方法。 --《Effective Java》

為什麼說不會被反射建立物件呢?查閱構造器反射例項化物件方法newInstance的原始碼可知:反射禁止了列舉物件的例項化,也就防止了反射攻擊,不用自己在構造器實現複雜的重複例項化邏輯了。

在這裡插入圖片描述

測試:

public class MainEnum {
 public static void main(String[] args) {
 SingletonEnum instance1 = SingletonEnum.INSTANCE;
 SingletonEnum instance2 = SingletonEnum.INSTANCE;
 System.out.println(instance1.hashCode());
 System.out.println(instance2.hashCode());
 }
}

在這裡插入圖片描述

總結:幾種實現方式的優缺點 懶漢模式

優點:節省記憶體。

缺點:存線上程安全問題,若要保證執行緒安全,則寫法複雜。

餓漢模式

優點:執行緒安全。

缺點:如果單例物件一直沒被使用,則會浪費記憶體空間。

靜態內部類

優點:懶載入並避免了多執行緒問題,寫法相比於懶漢模式更簡單。

缺點:需要多建立一個內部類。

列舉

優點:簡潔、天生執行緒安全、不可反射建立例項。

缺點:暫無

到此這篇關於Java單例模式的8種寫法的文章就介紹到這了,更多相關Java單例模式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!