目錄java
- 雙重檢測鎖的演變過程
- 利用HappensBefore分析併發問題
- 無volatile的雙重檢測鎖
雙重檢測鎖的最初形態是經過在方法聲明的部分加上synchronized進行同步,保證同一時間調用方法的線程只有一個,從而保證new Singlton()
的線程安全:安全
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這樣作的好處是代碼簡單、而且JVM保證new Singlton()
這行代碼線程安全。可是付出的代價有點高昂:
全部的線程的每一次調用都是同步調用,性能開銷很大,並且new Singlton()
只會執行一次,不須要每一次都進行同步。多線程
既然只須要在new Singlton()
時進行同步,那麼把synchronized
的同步範圍縮小呢?併發
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
把synchronized
同步的範圍縮小之後,貌似是解決了每次調用都須要進行同步而致使的性能開銷的問題。可是有引入了新的問題:線程不安全,返回的對象可能尚未初始化。
app
深刻到字節碼的層面來看看下面這段代碼:性能
instance = new Singleton() returen instance;
正常狀況下JVM編譯成成字節碼,它是這樣的:線程
step.1 new:開闢一塊內存空間 step.2 invokespecial:執行初始化方法,對內存進行初始化 step.3 putstatic:將該內存空間的引用賦值給instance step.4 areturn:方法執行結束,返回instance
固然這裏限定在正常狀況下,在特殊狀況下也能夠編譯成這樣:code
step.1 new:開闢一塊內存空間 step.3 putstatic:將該內存空間的引用賦值給instance step.2 invokespecial:執行初始化方法,對內存進行初始化 step.4 areturn:方法執行結束,返回instance
步驟2和步驟3進行了調換:先執行步驟3再執行步驟2。對象
這種特殊狀況稱之爲:指令重排序
:CPU採用了容許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。固然不是亂排序,重排序保證CPU可以正確處理指令依賴狀況以保障程序可以得出正確的執行結果。排序
HappensBefore
:先行發生,是
換句話說,能夠經過HappensBefore推斷代碼在多線程下是否線程安全
舉一個《深刻理解Java虛擬機》上的例子:
//如下操做在線程A中執行 int i = 1; //如下操做在線程B中執行 j = i; //如下操做在線程C中執行 i = 2;
若是hb(i=1
,j=i
),那麼能夠肯定變量j的值必定等於1。得出這個結論的依據有兩個:
i=1
的結果能夠被j=i
觀察到若是線程C的執行時間在線程A和線程B之間,那麼j
的值是多少呢?答案是不肯定!由於線程C和線程B之間沒有HappensBefore的關係:線程C對變量的i
的更改可能被線程B觀察到也可能不會!
這些是「自然的」、JVM保證的HappensBefore關係:
重點介紹程序次序規則
,管程鎖定規則
,volatile變量規則
,傳遞性
,後面分析須要用到這四個性質:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 instance = new Singleton(); //4 new //4.1 invokespecial //4.2 pustatic //4.3 } } } return instance; //5 } }
通過上面的討論,已經知道由於JVM重排序致使代碼4.2
提早執行了,致使後面一個線程執行代碼1
返回的值爲false,進而直接返回了尚未構造好的instance對象:
線程1 | 線程2 |
---|---|
1 | |
2 | |
3 | |
4.1 | |
4.3 | |
1 | |
5 | |
4.2 | |
5 |
經過表格,可能清晰看到問題所在:線程1代碼4.3 執行後,線程2執行代碼1讀到了髒數據。要想不讀到髒數據,只要證實存在hb(T1-4.3,T2-1)(T1-4表示線程1代碼4,T2-1表示線程2代碼1,下同),那麼是否存在呢?很遺憾,不存在:
用HappensBefore分析,能夠很清晰、明確看到沒有volatile修飾的雙重檢測鎖是線程不安全的。但,真的是這樣的嗎?
在第二部分,經過HappensBefore分析沒有volatile修飾的雙重檢測鎖是線程不安全,那只有用volatile修飾的雙重檢測鎖纔是線程安全的嗎?答案是否認的。
用volatile關鍵字修飾的本質是想利用volatile變量規則
,使得寫操做(T1-4)HappensBefore讀操做(T2-1),那隻要另找一條HappensBefore規則保證便可。答案是程序次序規則
和管程鎖定規則
先看代碼:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6 } } } return instance; //7 } }
在原有的基礎上加了兩行代碼:
instance = new Singleton(); //4 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6
爲何要這麼作?
經過管程鎖定規則保證執行到代碼6
時,temp對象已經構造好了。想想,爲何?
管程鎖定規則
開始生效:保證當前線程必定可以觀察到T1-6操做執行流程多是這樣的:
線程1 | 線程2 | 線程3 |
---|---|---|
1 | ||
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
2 | ||
3 | ||
1 | 7 | |
7 | ||
7 |
不管怎樣執行,其餘線程都可以觀察到T1-6的寫操做
內存屏障。
JVM在凡有volatile、synchronized出現的地方都加了一道內存屏障:重排序時,不能夠把內存屏障後面的指令重排序到內存屏障前面執行,而且會及時的將線程工做內存中的數據及時更新到主內存中,進而使得其餘的線程可以觀察到最新的數據
參考資料