從單例模式到HappensBefore

目錄java

  • 雙重檢測鎖的演變過程
  • 利用HappensBefore分析併發問題
  • 無volatile的雙重檢測鎖

雙重檢測鎖的演變過程

synchronized修飾方法的單例模式

雙重檢測鎖的最初形態是經過在方法聲明的部分加上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。對象

  • 若是隻有一個線程調用是沒有問題的:由於無論步驟如何調換,JVM保證返回的對象是已經構造好了。
  • 若是同時有多個線程調用,那麼部分調用線程返回的對象有多是沒有構造好的對象。

這種特殊狀況稱之爲:指令重排序:CPU採用了容許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。固然不是亂排序,重排序保證CPU可以正確處理指令依賴狀況以保障程序可以得出正確的執行結果。排序

利用HappensBefore分析併發問題

什麼是HappensBefore

HappensBefore:先行發生,是

  • 判斷數據是否存在競爭、線程是否安全的重要依據
  • A happens-beforeB,那麼A對B可見(A作的操做對B可見)
  • 是一種偏序關係。hb(a,b),hb(b,c) => hb(a,c)

換句話說,能夠經過HappensBefore推斷代碼在多線程下是否線程安全

舉一個《深刻理解Java虛擬機》上的例子:

//如下操做在線程A中執行
int i = 1;

//如下操做在線程B中執行
j = i;

//如下操做在線程C中執行
i = 2;

若是hb(i=1,j=i),那麼能夠肯定變量j的值必定等於1。得出這個結論的依據有兩個:

  1. 根據HappensBefore的規則,i=1的結果能夠被j=i觀察到
  2. 線程C尚未登場

若是線程C的執行時間在線程A和線程B之間,那麼j的值是多少呢?答案是不肯定!由於線程C和線程B之間沒有HappensBefore的關係:線程C對變量的i的更改可能被線程B觀察到也可能不會!

HappensBefore關係

這些是「自然的」、JVM保證的HappensBefore關係:

  1. 程序次序規則
  2. 管程鎖定規則
  3. volatile變量規則
  4. 線程啓動規則
  5. 線程終止規則
  6. 線程中斷規則
  7. 對象終結規則
  8. 傳遞性

重點介紹程序次序規則管程鎖定規則volatile變量規則傳遞性,後面分析須要用到這四個性質:

  • 程序次序規則:在一個線程內,按照程序控制流順序,書寫在前面的操做HappensBefore書寫在後面的操做
  • 管程鎖定規則:對於同一個鎖來講,在時間順序上,上一個unlock操做HappensBefore下一個lock操做
  • volatile變量規則:對於一個volatile修飾的變量,在時間順序上,寫操做HappensBefore讀操做
  • 傳遞性:hb(a,b),hb(b,c) => hb(a,c)

分析以前線程不安全的雙重檢測鎖

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,下同),那麼是否存在呢?很遺憾,不存在:

  • 程序次序規則:不在同一個線程
  • 管程鎖定規則:線程2沒有嘗試lock
  • volatile變量規則:instance對象沒有經過volatile關鍵字修飾
  • 傳遞性:不存在

用HappensBefore分析,能夠很清晰、明確看到沒有volatile修飾的雙重檢測鎖是線程不安全的。但,真的是這樣的嗎?

無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對象已經構造好了。想想,爲何?

  1. 其餘線程執行代碼1時,若是可以觀察到T1-6的寫操做,那麼直接返回instance對象
  2. 若是沒有觀察到T1-6的寫操做,那麼嘗試獲取鎖,此時管程鎖定規則開始生效:保證當前線程必定可以觀察到T1-6操做

執行流程多是這樣的:

線程1 線程2 線程3
1
1
2
3
4
5
6
2
3
1 7
7
7

不管怎樣執行,其餘線程都可以觀察到T1-6的寫操做

其餘

volatile、synchronized爲何能夠禁止JVM重排序

內存屏障。

JVM在凡有volatile、synchronized出現的地方都加了一道內存屏障:重排序時,不能夠把內存屏障後面的指令重排序到內存屏障前面執行,而且會及時的將線程工做內存中的數據及時更新到主內存中,進而使得其餘的線程可以觀察到最新的數據


參考資料

  1. 《深刻理解Java虛擬機》
相關文章
相關標籤/搜索