一、案例
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帶來的性能開銷。上面代碼表面上看起來,似乎兩全其美:
- 在多個線程試圖在同一時間建立對象時,會通過加鎖來保證隻有一個線程能建立對象。
- 在對象建立好之後,執行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;
}
}