Volatile的應用DCL單例模式
多執行緒環境下的單例模式的併發問題
首先回顧一下,單執行緒下的單例模式程式碼
1 /** 2 * 單例模式 3 * 4 * @author xiaocheng 5 * @date 2020/4/22 9:19 6 */ 7 public class Singleton { 8 9 private static Singleton singleton = null; 10 11 private Singleton() { 12 System.out.println(Thread.currentThread().getName() + "\t單例構造方法");13 } 14 15 public static Singleton getInstance() { 16 if (singleton == null) { 17 singleton = new Singleton(); 18 } 19 return singleton; 20 } 21 22 public static void main(String[] args) { 23 System.out.println(Singleton.getInstance() == Singleton.getInstance());24 System.out.println(Singleton.getInstance() == Singleton.getInstance()); 25 System.out.println(Singleton.getInstance() == Singleton.getInstance()); 26 System.out.println(Singleton.getInstance() == Singleton.getInstance()); 27 } 28 }
最後輸出的結果
但是在多執行緒的環境下,我們的單例模式是否還是同一個物件了
1 /** 2 * 單例模式 3 * 4 * @author xiaocheng 5 * @date 2020/4/22 9:19 6 */ 7 public class Singleton { 8 9 private static Singleton singleton = null; 10 11 private Singleton() { 12 System.out.println(Thread.currentThread().getName() + "\t單例構造方法"); 13 } 14 15 public static Singleton getInstance() { 16 if (singleton == null) { 17 singleton = new Singleton(); 18 } 19 return singleton; 20 } 21 22 public static void main(String[] args) { 23 for (int i = 0; i < 10; i++) { 24 new Thread(() -> { 25 Singleton.getInstance(); 26 }, String.valueOf(i)).start(); 27 } 28 } 29 }
從下面的結果我們可以看出,我們通過SingletonDemo.getInstance() 獲取到的物件,並不是同一個,而是被下面幾個執行緒都進行了建立,那麼在多執行緒環境下,單例模式如何保證呢?
解決方法1
引入synchronized關鍵字
1 public synchronized static SingletonDemo getInstance() { 2 if(instance == null) { 3 instance = new SingletonDemo(); 4 } 5 return instance; 6 }
輸出結果
我們能夠發現,通過引入Synchronized關鍵字,能夠解決高併發環境下的單例模式問題
但是synchronized屬於重量級的同步機制,它只允許一個執行緒同時訪問獲取例項的方法,但是為了保證資料一致性,而減低了併發性,因此採用的比較少
解決方法2
通過引入DCL Double Check Lock 雙端檢鎖機制
就是在進來和出去的時候,進行檢測
public static SingletonDemo getInstance() { if(instance == null) { // 同步程式碼段的時候,進行檢測 synchronized (SingletonDemo.class) { if(instance == null) { instance = new SingletonDemo(); } } } return instance; }
最後輸出的結果為:
從輸出結果來看,確實能夠保證單例模式的正確性,但是上面的方法還是存在問題的
DCL(雙端檢鎖)機制不一定是執行緒安全的,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一個執行緒執行到第一次檢測的時候,讀取到 instance 不為null,instance的引用物件可能沒有完成例項化。因為 instance = new SingletonDemo();可以分為以下三步進行完成:
- memory = allocate(); // 1、分配物件記憶體空間
- instance(memory); // 2、初始化物件
- instance = memory; // 3、設定instance指向剛剛分配的記憶體地址,此時instance != null
但是我們通過上面的三個步驟,能夠發現,步驟2 和 步驟3之間不存在 資料依賴關係,而且無論重排前 還是重排後,程式的執行結果在單執行緒中並沒有改變,因此這種重排優化是允許的。
- memory = allocate(); // 1、分配物件記憶體空間
- instance = memory; // 3、設定instance指向剛剛分配的記憶體地址,此時instance != null,但是物件還沒有初始化完成
- instance(memory); // 2、初始化物件
這樣就會造成什麼問題呢?
也就是當我們執行到重排後的步驟2,試圖獲取instance的時候,會得到null,因為物件的初始化還沒有完成,而是在重排後的步驟3才完成,因此執行單例模式的程式碼時候,就會重新在建立一個instance例項
指令重排只會保證序列語義的執行一致性(單執行緒),但並不會關係多執行緒間的語義一致性
所以當一條執行緒訪問instance不為null時,由於instance例項未必已初始化完成,這就造成了執行緒安全的問題
所以需要引入volatile,來保證出現指令重排的問題,從而保證單例模式的執行緒安全性
private static volatile SingletonDemo instance = null;
最終程式碼
/** * 單例模式 * * @author xiaocheng * @date 2020/4/22 9:19 */ public class Singleton { private static volatile Singleton singleton = null; private Singleton() { System.out.println(Thread.currentThread().getName() + "\t單例構造方法"); } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } public static void main(String[] args) { // System.out.println(Singleton.getInstance() == Singleton.getInstance()); // System.out.println(Singleton.getInstance() == Singleton.getInstance()); // System.out.println(Singleton.getInstance() == Singleton.getInstance()); // System.out.println(Singleton.getInstance() == Singleton.getInstance()); for (int i = 0; i < 10; i++) { new Thread(() -> { Singleton.getInstance(); }, String.valueOf(i)).start(); } } }