天天看點

java單例模式-指令重排陷阱一、案例

一、案例

public class SingletonTest {
	 private static SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static SingletonTest getInstance() {
	    if (SingletonTest == null) {
	        SingletonTest = new SingletonTest();
	    }
	    return SingletonTest;
	 }
}
           

這是一個懶漢式的單例實作,衆所周知,因為沒有相應的鎖機制,這個程式是線程不安全的,實作安全的最快捷的方式是添加 synchronized

public class SingletonTest {
	 private static SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static synchronized  SingletonTest getInstance() {
	    if (SingletonTest == null) {
	        SingletonTest = new SingletonTest();
	    }
	    return SingletonTest;
	 }
}
           

使用synchronized之後,可以保證線程安全,但是synchronized将全部代碼塊鎖住,這樣會導緻較大的性能開銷,是以,人們想出了一個“聰明”的技巧:雙重檢查鎖DCL(double checked locking)的機制實作單例。

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

如上面代碼所示,如果第一次檢查instance不為null,那麼就不需要執行下面的加鎖和初始化操作。是以可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來,似乎兩全其美:

  1. 在多個線程試圖在同一時間建立對象時,會通過加鎖來保證隻有一個線程能建立對象。
  2. 在對象建立好之後,執行getInstance()将不需要擷取鎖,直接傳回已建立好的對象。

程式看起來很完美,但是這是一個不完備的優化,線上程執行到第9行代碼讀取到instance不為null時(第一個if),instance引用的對象有可能還沒有完成初始化。

問題的根源

問題出現在建立對象的語句

singleton3 = new Singleton3();

 上,在java中建立一個對象并非是一個原子操作,可以被分解成三行僞代碼:

//1:配置設定對象的記憶體空間
memory = allocate();
//2:初始化對象
ctorInstance(memory);  
//3:設定instance指向剛配置設定的記憶體位址
instance = memory;   
           

上面三行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器中),即編譯器或處理器為提高性能改變代碼執行順序,這一部分的内容稍後會詳細解釋,重排序之後的僞代碼是這樣的:

//1:配置設定對象的記憶體空間
memory = allocate(); 
//3:設定instance指向剛配置設定的記憶體位址
instance = memory;
//2:初始化對象
ctorInstance(memory);
           

在單線程程式下,重排序不會對最終結果産生影響,但是并發的情況下,可能會導緻某些線程通路到未初始化的變量。

模拟一個2個線程建立單例的場景,如下表:

時間 線程A 線程B
t1 A1:配置設定對象記憶體空間
t2 A3:設定instance指向記憶體空間
t3 B1:判斷instance是否為空
t4 B2:由于instance不為null,線程B将通路instance引用的對象
t5 A2:初始化對象
t6 A4:通路instance引用的對象

按照這樣的順序執行,線程B将會獲得一個未初始化的對象,并且自始至終,線程B無需擷取鎖!

雙重檢查鎖問題解決方案

解決方案就是大名鼎鼎的volatile關鍵字,對于volatile我們最深的印象是它保證了”可見性“,它的”可見性“是通過它的記憶體語義實作的:

  • 寫volatile修飾的變量時,JMM會把本地記憶體中值重新整理到主記憶體
  • 讀volatile修飾的變量時,JMM會設定本地記憶體無效
public class SingletonTest {
	 private static volatile  SingletonTest SingletonTest;
	 private SingletonTest() {
	 }
	 public static synchronized  SingletonTest getInstance() {
		 if (SingletonTest == null) {
	            synchronized (SingletonTest.class) {
	                if (SingletonTest == null) {
	                    SingletonTest = new SingletonTest();
	                }
	            }
	        }
	        return SingletonTest;
	 }
}