如果Java內(nèi)存模型中所有的有序性都僅僅靠volatile和synchronized來完成,那么有一些操作將會(huì)變得很煩瑣,但是我們?cè)诰帉慗ava并發(fā)代碼的時(shí)候并沒有感覺到這一點(diǎn),這是因?yàn)镴ava語言中有一個(gè)“先行發(fā)生”(happens-before)的原則。 這個(gè)原則非常重要,它是判斷數(shù)據(jù)是否存在競爭、 線程是否安全的主要依據(jù),依靠這個(gè)原則,我們可以通過幾條規(guī)則一攬子地解決并發(fā)環(huán)境下兩個(gè)操作之間是否可能存在沖突的所有問題。
一、什么是先行發(fā)生原則
現(xiàn)在就來看看“先行發(fā)生”原則指的是什么。 先行發(fā)生是Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,如果說操作A先行發(fā)生于操作B,其實(shí)就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到?!坝绊憽卑ㄐ薷牧藘?nèi)存中共享變量的值、 發(fā)送了消息、 調(diào)用了方法等。 這句話不難理解,但它意味著什么呢?我們可以舉個(gè)例子來說明一下,如下偽代碼:
//以下操作在線程A中執(zhí)行
i=1;
//以下操作在線程B中執(zhí)行
j=i;
//以下操作在線程C中執(zhí)行
i=2;
假設(shè)線程A中的操作“i=1”先行發(fā)生于線程B的操作“j=i”,那么可以確定在線程B的操作執(zhí)行后,變量j的值一定等于1,得出這個(gè)結(jié)論的依據(jù)有兩個(gè):一是根據(jù)先行發(fā)生原則,“i=1”的結(jié)果可以被觀察到;二是線程C還沒“登場”,線程A操作結(jié)束之后沒有其他線程會(huì)修改變量i的值。 現(xiàn)在再來考慮線程C,我們依然保持線程A和線程B之間的先行發(fā)生關(guān)系,而線程C出現(xiàn)在線程A和線程B的操作之間,但是線程C與線程B沒有先行發(fā)生關(guān)系,那j的值會(huì)是多少呢?答案是不確定!1和2都有可能,因?yàn)榫€程C對(duì)變量i的影響可能會(huì)被線程B觀察到,也可能不會(huì),這時(shí)候線程B就存在讀取到過期數(shù)據(jù)的風(fēng)險(xiǎn),不具備多線程安全性。
二、Java內(nèi)存模型中的先行發(fā)生關(guān)系
下面是Java內(nèi)存模型下一些“天然的”先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。 如果兩個(gè)操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們隨意地進(jìn)行重排序:
- 程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。 準(zhǔn)確地說,應(yīng)該是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、 循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。 這里必須強(qiáng)調(diào)的是同一個(gè)鎖,而“后面”是指時(shí)間上的先后順序。
- volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后順序。
- 線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
- 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、 Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
- 對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
- 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
三、如何應(yīng)用先行發(fā)生規(guī)則
Java語言無須任何同步手段保障就能成立的先行發(fā)生規(guī)則就只有上面這些了,筆者演示一下如何使用這些規(guī)則去判定操作間是否具備順序性,對(duì)于讀寫共享變量的操作來說,就是線程是否安全,讀者還可以從下面這個(gè)例子中感受一下“時(shí)間上的先后順序”與“先行發(fā)生”之間有什么不同:
private int value=0;
pubilc void setValue(int value){
this.value=value;
}
public int getValue(){
return value;
}
以上顯示的是一組再普通不過的getter/setter方法,假設(shè)存在線程A和B,線程A先(時(shí)間上的先后)調(diào)用了“setValue(1)”,然后線程B調(diào)用了同一個(gè)對(duì)象的“getValue()”,那么線程B收到的返回值是什么?
我們依次分析一下先行發(fā)生原則中的各項(xiàng)規(guī)則,由于兩個(gè)方法分別由線程A和線程B調(diào)用,不在一個(gè)線程中,所以程序次序規(guī)則在這里不適用;由于沒有同步塊,自然就不會(huì)發(fā)生lock和unlock操作,所以管程鎖定規(guī)則不適用;由于value變量沒有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;后面的線程啟動(dòng)、 終止、 中斷規(guī)則和對(duì)象終結(jié)規(guī)則也和這里完全沒有關(guān)系。 因?yàn)闆]有一個(gè)適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無從談起,因此我們可以判定盡管線程A在操作時(shí)間上先于線程B,但是無法確定線程B中“getValue()”方法的返回結(jié)果,換句話說,這里面的操作不是線程安全的。
那怎么修復(fù)這個(gè)問題呢?我們至少有兩種比較簡單的方案可以選擇:要么把getter/setter方法都定義為synchronized方法,這樣就可以套用管程鎖定規(guī)則;要么把value定義為volatile變量,由于setter方法對(duì)value的修改不依賴value的原值,滿足volatile關(guān)鍵字使用場景,這樣就可以套用volatile變量規(guī)則來實(shí)現(xiàn)先行發(fā)生關(guān)系。
通過上面的例子,我們可以得出結(jié)論:一個(gè)操作“時(shí)間上的先發(fā)生”不代表這個(gè)操作會(huì)是“先行發(fā)生”,那如果一個(gè)操作“先行發(fā)生”是否就能推導(dǎo)出這個(gè)操作必定是“時(shí)間上的先發(fā)生”呢?很遺憾,這個(gè)推論也是不成立的,一個(gè)典型的例子就是多次提到的“指令重排序”,演示例子如下代碼所示:
//以下操作在同一個(gè)線程中執(zhí)行
int i=1;
int j=2;
以上代碼的兩條賦值語句在同一個(gè)線程之中,根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因?yàn)槲覀冊(cè)谶@條線程之中沒有辦法感知到這點(diǎn)。
上面兩個(gè)例子綜合起來證明了一個(gè)結(jié)論:時(shí)間先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時(shí)候不要受到時(shí)間順序的干擾,一切必須以先行發(fā)生原則為準(zhǔn)。