双重检查(Double-Check)
有一個優化的思路,就是把100%出現的防護盾,也改為1%的幾率出現,使之只出現在可能會導致多個實例出現的地方。
代碼如下:
// 代碼三 public class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null){synchronized(Singleton.class){if (instance == null)instance = new Singleton();}}return instance;} }這段代碼看起來有點復雜,注意其中有兩次if(instance==null)的判斷,這個叫做『雙重檢查 Double-Check』。
- 第一個 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 件事情。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
3. 將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就執行了讀操作。
對于代碼三出現的問題,解決方案為:給instance的聲明加上volatile關鍵字
代碼如下:
public class Singleton {private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null){synchronized(Singleton.class){if (instance == null)instance = new Singleton();}}return instance;} }volatile關鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內存屏障,這樣,在它的賦值完成之前,就不用會調用讀操作。
注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調用讀操作(if (instance == null))。
?
總結
以上是生活随笔為你收集整理的双重检查(Double-Check)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 下游传递唯一序列号如何实现幂等性?
- 下一篇: C 基本语法 | 菜鸟教程