Happens-Before原則(先行發(fā)生原則)

Happens-Before

從jdk5開始,java使用新的JSR-133內(nèi)存模型,基于Happens-Before的概念來闡述操作之間的內(nèi)存可見性。

Happens-Before定義

  1. 如果一個操作Happens-Before另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
  2. 兩個操作之間存在Happens-Before關(guān)系,并不意味著一定要按照Happens-Before原則制定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果與按照Happens-Before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法。

注意:不能將Happens-Before理解為它的字面意思,可以理解為“先行發(fā)生”,如A先行發(fā)生于B,就是說B執(zhí)行之前,A產(chǎn)生的影響(修改共享變量、發(fā)送消息、調(diào)用方法等)可以被B觀察到。(一團漿糊...繼續(xù)挖)

Happens-Before規(guī)則

Happens-Before的八個規(guī)則(摘自《深入理解Java虛擬機》12.3.6章節(jié)):

  1. 程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
  2. 管程鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作;(此處后面指時間的先后)
  3. volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作;(此處后面指時間的先后)
  4. 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作;
  5. 線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行;
  6. 線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生;
  7. 對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始;
  8. 傳遞性:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C;

Happens-Before規(guī)則詳解

程序次序規(guī)則

同一個線程內(nèi),書寫在前面的操作先行發(fā)生于書寫在后面的操作:在網(wǎng)上有看到過很多文章,但是實際編譯時經(jīng)過指令重排序,有些情況下書寫在后面的代碼會先于前面的代碼。Happens-Before可以理解為前面代碼的執(zhí)行結(jié)果對于后面代碼是可見的(...怎么說有點繞,看例子吧)。

int a = 3;     //代碼1
int b = a + 1; //代碼2

上面的代碼中,因為代碼2的計算會用到代碼1的運行結(jié)果,此時程序次序規(guī)則就會保證代碼2中的a一定為3,不會是0(默認初始化的值),所以JVM不允許操作系統(tǒng)對代碼1、2進行重排序,即代碼1一定在代碼2之前執(zhí)行。下面的例子就無法保證執(zhí)行順序:

int a = 3; //代碼1
int b = 2; //代碼2

上面的代碼中,代碼1、2之間沒有依賴關(guān)系,所以指令重排序有可能會發(fā)生,b的初始化可能比a早。

管程鎖定規(guī)則

一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作:同一個鎖只能由一個線程持有,下面舉例

public class TestHappenBefore {
    public static int var;
    private static TestHappenBefore happenBefore = new TestHappenBefore();

    public static TestHappenBefore getInstance() {
        return happenBefore;
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> TestHappenBefore.getInstance().method2()).start();
        new Thread(() -> TestHappenBefore.getInstance().method1()).start();
        new Thread(() -> TestHappenBefore.getInstance().method3()).start();
    }

    public synchronized void method1() {
        var = 3;
        System.out.println("method1,var:" + var);
    }

    public synchronized void method2() {
        try {
            System.out.println("線程2開始睡覺了~");
            new Thread().sleep(5000);
            System.out.println("線程2睡好了~");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int b = var;
        System.out.println("method2,var:" + var + ",b:" + b);
    }

    public void method3() {
        synchronized (new TestHappenBefore()) { //換了把新鎖
            var = 4;
            System.out.println("method3,var:" + var);
        }
    }
}
執(zhí)行結(jié)果:
線程2開始睡覺了~
method3,var:4
線程2睡好了~
method2,var:4,b:4
method1,var:3

通過上面的例子我們發(fā)現(xiàn),當(dāng)線程2在“睡覺”的時間段內(nèi),線程1并沒有執(zhí)行,因為此時happenBefore對象的鎖被線程2持有,線程2釋放鎖之前,線程1無法持有該鎖,這符合管程鎖定規(guī)則,還發(fā)現(xiàn)線程2“睡覺”的時候,線程3并沒有停下,仍然執(zhí)行了自己的代碼,是因為method3的鎖和線程2不是同一把鎖,所以不受管程鎖定規(guī)則的限制。

volatile變量規(guī)則

對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作(此處后面指時間的先后):這條規(guī)則保證了volatile變量的可見性,線程A寫volatile變量后,線程B讀volatile變量,則B讀到的一定是A寫的值,照舊舉例(沒有寫出合適的案例,附上偽代碼說明,如有合適的案例,請指教):

volatile int a;
//線程1執(zhí)行內(nèi)容
public void method1() {
    a = 1;
}
//線程2執(zhí)行內(nèi)容
public void method2() {
    int b = a;
}

如果線程1先執(zhí)行,線程2再執(zhí)行,則volatile變量規(guī)則可以保證線程2讀取的變量a的值為1。

傳遞性

如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C(感覺類似數(shù)學(xué)的傳遞性:A>B,B>C則A>C...),照舊一例:

volatile int var;
int b;
int c;
//線程1執(zhí)行內(nèi)容
public void method1() {
    b = 4; //1
    var = 3; //2
}
//線程2執(zhí)行內(nèi)容
public void method2() {
    c = var; //3
    c = b; //4
}

假設(shè)執(zhí)行順序為 1、2、3、4,由于單線程的程序次序規(guī)則,得出1 Happen Before 2,3 Happen Before 4,又因為volatile變量規(guī)則得出2 Happen Before 3,所以1 Happen Before 3,1 Happen Before 4(傳遞性),即最后變量c的值為4;若執(zhí)行順序為1、3、4、2,因為3、2沒有匹配到Happen Before規(guī)則,所以無法通過傳遞性推測出傳遞關(guān)系,也就無法保證最后變量c的值為4,也可能為0(b初始化的值,沒有讀到線程1寫入的值)

線程啟動規(guī)則、線程終結(jié)規(guī)則、線程中斷規(guī)則、對象終結(jié)規(guī)則四個規(guī)則相對比較易于理解,不再贅述。

Happens-Before原則與時間順序的關(guān)系

前面提到不可以將Happens-Before理解為它的字面意思,即不能站在時間順序的角度去理解先行發(fā)生原則,通過下面的例子來驗證一下:

private int value = 0;
public void setValue(int value){
    this.value = value;
}
public void getValue(){
    return value;
}

假設(shè)線程A調(diào)用setValue(1)方法,線程B調(diào)用同對象的getValue()方法,線程A在時間上先執(zhí)行,此時線程B調(diào)用方法的返回值是什么?
依次分析一下先行發(fā)生的八大原則:例子不在同一個線程內(nèi),故程序次序規(guī)則不適用;代碼中沒有同步塊,所以管程鎖定規(guī)則不適用;變量value沒有被volatile關(guān)鍵字修飾,volatile變量規(guī)則同樣不適用;線程啟動規(guī)則、線程終結(jié)規(guī)則、線程中斷規(guī)則、對象終結(jié)規(guī)則和本例沒有關(guān)系。因為沒有匹配到任何一條規(guī)則,所以傳遞性也不適用。通過執(zhí)行結(jié)果(具有一定偶然性,實驗時加大循環(huán)次數(shù)),我們會發(fā)現(xiàn)B的返回值有可能是1有可能是0,所以這個操作不是線程安全的。
解決方式有多種,例如:getter、setter方法加上synchronized同步塊,就可以匹配上管程鎖定規(guī)則;或者value變量用volatile關(guān)鍵字進行修飾,則可以匹配上volatile變量規(guī)則。
通過這個例子我們可以得出:“時間上的先發(fā)生”不代表這個操作是“先行發(fā)生”。
那“先行發(fā)生”的操作一定是“時間上的先發(fā)生”么?答案是否定的,最典型的例子就是我們常說的“指令重排序”,例子如下:

// 同一線程內(nèi)
int i=1;
int j=1;

上面代碼運行情況符合程序次序規(guī)則,按規(guī)則應(yīng)該是“int i = 1;”的操作先行發(fā)生于“int j = 2;”,但“int j = 2;”有可能會先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因為我們的線程無法感知這點。
通過上面的兩個例子,我們得出:時間的先后順序和先行發(fā)生原則(Happen-Before原則)基本沒有關(guān)系,所以我們在排查線程安全問題的時候不要受到時間順序的干擾,一切以先行發(fā)生原則(Happen-Before原則)為準(zhǔn)(摘自《深入理解Java虛擬機》12.3.6章節(jié))。

文章參考:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容