看似完美的單例模式?
public class Single {
private static Single3 instance;
private Single() {}
public static Single getInstance() {
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
- 第一個if (instance == null),其實是為了解決效率問題,只有instance為null的時候,才進入synchronized的代碼段——大大減少了幾率。
- 第二個if (instance == null),則是為了防止可能出現多個實例的情況。
那么還會有問題嗎?
答:只是『看起來』,還是有小概率出現問題的。
原子操作
原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為線程調度被打斷的操作。
例子:
m = 6; // 這是個原子操作
假如m原先的值為0,那么對于這個操作,要么執行成功m變成了6,要么是沒執行m還是0,而不會出現諸如m=3這種中間態——即使是在并發的線程中。
而,聲明并賦值就不是一個原子操作:
int n = 6; // 這不是一個原子操作
對于這個語句,至少有兩個操作:
① 明一個變量n
② 給n賦值為6
——這樣就會有一個中間狀態:變量n已經被聲明了但是還沒有被賦值的狀態。
——這樣,在多線程中,由于線程執行順序的不確定性,如果兩個線程都使用m,就可能會導致不穩定的結果出現。
指令重排
概念
簡單來說,就是計算機為了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。
例子:
int a ; // 語句1
a = 8 ; // 語句2
int b = 9 ; // 語句3
int c = a + b ; // 語句4
正常來說,對于順序結構,執行的順序是自上到下,也即1234。
但是,由于指令重排的原因,因為不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324。
由于語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。
——也就是說,
對于非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。
回到話題
主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
- 給 singleton 分配內存
- 調用 Singleton 的構造函數來初始化成員變量,形成實例
- 將singleton對象指向分配的內存空間(執行完這步 singleton才是非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
錯誤根源:
再稍微解釋一下,就是說,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他線程剛好運行到第一層if (instance == null)這里,這里讀取到的instance已經不為null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。這里的關鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執行了讀操作。
完全版
public class Single {
private static volatile Single4 instance;
private Single() {}
public static Single getInstance() {
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single();
}
}
}
return instance;
}
}
volatile發揮的作用
volatile關鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內存屏障(什么是內存屏障?),這樣,在它的賦值完成之前,就不用會調用讀操作。
注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調用讀操作(if (instance == null))。