Happens-Before
從jdk5開始,java使用新的JSR-133內存模型沥割,基于Happens-Before的概念來闡述操作之間的內存可見性盲憎。
Happens-Before定義
- 如果一個操作Happens-Before另一個操作戒劫,那么第一個操作的執(zhí)行結果將對第二個操作可見半夷,而且第一個操作的執(zhí)行順序排在第二個操作之前婆廊。
- 兩個操作之間存在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é)):
- 程序次序規(guī)則:一個線程內蔬蕊,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作哥谷;
- 管程鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作岸夯;(此處后面指時間的先后)
- volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作;(此處后面指時間的先后)
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作们妥;
- 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測猜扮,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執(zhí)行监婶;
- 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生破镰;
- 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始;
- 傳遞性:如果操作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é))。