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

Happens-Before

從jdk5開始,java使用新的JSR-133內存模型沥割,基于Happens-Before的概念來闡述操作之間的內存可見性盲憎。

Happens-Before定義

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

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

Happens-Before規(guī)則

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

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

Happens-Before規(guī)則詳解

程序次序規(guī)則

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

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

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

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

上面的代碼中鹉究,代碼1、2之間沒有依賴關系踪宠,所以指令重排序有可能會發(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í)行結果:
線程2開始睡覺了~
method3,var:4
線程2睡好了~
method2肖粮,var:4孤页,b:4
method1,var:3

通過上面的例子我們發(fā)現(xiàn)涩馆,當線程2在“睡覺”的時間段內行施,線程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í)行內容
public void method1() {
    a = 1;
}
//線程2執(zhí)行內容
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ù)學的傳遞性:A>B,B>C則A>C...),照舊一例:

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

假設執(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ī)則采章,所以無法通過傳遞性推測出傳遞關系,也就無法保證最后變量c的值為4壶辜,也可能為0(b初始化的值悯舟,沒有讀到線程1寫入的值)

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

Happens-Before原則與時間順序的關系

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

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

假設線程A調用setValue(1)方法演侯,線程B調用同對象的getValue()方法承璃,線程A在時間上先執(zhí)行,此時線程B調用方法的返回值是什么蚌本?
依次分析一下先行發(fā)生的八大原則:例子不在同一個線程內,故程序次序規(guī)則不適用隘梨;代碼中沒有同步塊程癌,所以管程鎖定規(guī)則不適用;變量value沒有被volatile關鍵字修飾轴猎,volatile變量規(guī)則同樣不適用嵌莉;線程啟動規(guī)則、線程終結規(guī)則捻脖、線程中斷規(guī)則锐峭、對象終結規(guī)則和本例沒有關系中鼠。因為沒有匹配到任何一條規(guī)則,所以傳遞性也不適用沿癞。通過執(zhí)行結果(具有一定偶然性援雇,實驗時加大循環(huán)次數(shù)),我們會發(fā)現(xiàn)B的返回值有可能是1有可能是0椎扬,所以這個操作不是線程安全的惫搏。
解決方式有多種,例如:getter蚕涤、setter方法加上synchronized同步塊筐赔,就可以匹配上管程鎖定規(guī)則;或者value變量用volatile關鍵字進行修飾揖铜,則可以匹配上volatile變量規(guī)則茴丰。
通過這個例子我們可以得出:“時間上的先發(fā)生”不代表這個操作是“先行發(fā)生”。
那“先行發(fā)生”的操作一定是“時間上的先發(fā)生”么天吓?答案是否定的贿肩,最典型的例子就是我們常說的“指令重排序”,例子如下:

// 同一線程內
int i=1;
int j=1;

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

文章參考:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末冒签,一起剝皮案震驚了整個濱河市在抛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萧恕,老刑警劉巖刚梭,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異票唆,居然都是意外死亡朴读,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門走趋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衅金,“玉大人,你說我怎么就攤上這事〉ǎ” “怎么了鉴吹?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惩琉。 經常有香客問我豆励,道長,這世上最難降的妖魔是什么琳水? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任肆糕,我火速辦了婚禮,結果婚禮上在孝,老公的妹妹穿的比我還像新娘诚啃。我一直安慰自己,他們只是感情好私沮,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布始赎。 她就那樣靜靜地躺著,像睡著了一般仔燕。 火紅的嫁衣襯著肌膚如雪造垛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天晰搀,我揣著相機與錄音五辽,去河邊找鬼。 笑死外恕,一個胖子當著我的面吹牛杆逗,可吹牛的內容都是我干的。 我是一名探鬼主播罪郊,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼尚洽,長吁一口氣:“原來是場噩夢啊……” “哼腺毫!你這毒婦竟也來了?” 一聲冷哼從身側響起潮酒,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤澈灼,失蹤者是張志新(化名)和其女友劉穎叁熔,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遭贸,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡壕吹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年耳贬,在試婚紗的時候發(fā)現(xiàn)自己被綠了猎唁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡诫隅,死狀恐怖,靈堂內的尸體忽然破棺而出蛔屹,到底是詐尸還是另有隱情豁生,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布眼刃,位于F島的核電站,受9級特大地震影響擂红,放射性物質發(fā)生泄漏围小。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一变秦、第九天 我趴在偏房一處隱蔽的房頂上張望蹦玫。 院中可真熱鬧,春花似錦挣输、人聲如沸福贞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至你稚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搁痛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工宇弛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鸡典,地道東北人枪芒。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓彻况,卻偏偏與公主長得像舅踪,于是被迫代替她去往敵國和親纽甘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354