Volatile的應用DCL單例模式(四)
Volatile的應用
單例模式DCL代碼
首先回顧一下,單線程下的單例模式代碼
/**
* 單例模式
*
* @author xiaocheng
* @date 2020/4/22 9:19
*/
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
System.out.println(Thread.currentThread().getName() + "\t單例構造方法");
}
public static Singleton getInstance() {
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());
}
}
最後輸出的結果
但是在多線程的環境下,我們的單例模式是否還是同一個對象了
/**
* 單例模式
*
* @author xiaocheng
* @date 2020/4/22 9:19
*/
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
System.out.println(Thread.currentThread().getName() + "\t單例構造方法");
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Singleton.getInstance();
}, String.valueOf(i)).start();
}
}
}
從下面的結果我們可以看出,我們通過SingletonDemo.getInstance() 獲取到的對象,並不是同一個,而是被下面幾個線程都進行了創建,那麼在多線程環境下,單例模式如何保證呢?
解決方法1
引入synchronized關鍵字
public synchronized static SingletonDemo getInstance() {
if(instance == null) {
instance = new SingletonDemo();
}
return instance;
}
輸出結果
我們能夠發現,通過引入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();
}
}
}