先行發生原則(happens-before)
我們知道,在Java內存模型中,如果要確保有序性可以靠volatile和synchronized來實現,但是如果所有的有序性都僅僅依靠這兩個關鍵字來完成,那麼有一些操作將會變得很繁瑣,但是我們在編寫Java代碼的時候並沒有感覺到這一點,這是因爲Java語言中有一個“先行發生(happens-before)”的原則。那麼happens-before到底是什麼呢?
什麼是happens-before
happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。Leslie Lamport使用happens-before來定義分佈式系統中事件之間的偏序關係(partial ordering)。Leslie Lamport在這篇論文中給出了一個 分佈式算法,該算法可以將該偏序關係擴展爲某種全序關係。 JSR-133使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個線程之內,也可以是在不同線程之間。因此,JMM可以通過happens-before關係向程序員提供跨線程的內存可見性保證。
《JSR-133:Java Memory Model and Thread Specification》對happens-before關係的定義如下:
1)如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。注意:這一點僅僅是JMM對程序員的保證
2)兩個操作之間存在happens-before關係,並不意味着Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。
happens-before規則示例
Java內存模型中存在一些“天然的”happens-before關係,這些先行關係無需任何同步器的協助就已經存在,可以在編碼中直接使用(下面8條規則的定義參考了《深入理解Java虛擬機》一書,結合自己的理解部分規則給出了示例):
- 程序次序規則(Program Order Rule):在一個線程內,書寫在前面的操作先行發生於後面的操作。準確的說,應該是控制流順序而不是程序代碼順序,因爲要考慮分支和循環等結構。如下代碼示例:1 happens-before 2,3 happens-before 4
package com.zwx.happans.before;
public class VolatileRule {
private volatile boolean flag = false;
private int a = 0;
public void writer(){
a = 1;//1
flag = true;//2
}
public void read(){
if(flag){//3
int i = a;//4
}
}
}
- volatile 變量規則(Volatile Lock Rule):對於 volatile 修飾的變量的寫的操作,一定 happen-before 後續對於volatile變量的讀操作。如上順序規則示例中,因爲變量flag加了volatile修飾,所以2 happens-before 3
- 傳遞性規則(Transitivity Rule):如上順序規則示例中,根據順序規則:1 happens-before 2;根據volatile變量規則:2 happens-before 3;故而根據傳遞性規則可以推導出:1 happens-before 3。
- 線程啓動規則(Thread Start Rule):Thread對象的start()方法,先行發生行於此線程的每一個動作。如下示例中,因爲1 happens-before 2,而2又happens-before線程1內的所有操作,所有x的值對線程t1是可見的
package com.zwx.happans.before;
public class StartRule {
static int x = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//主線程修改x的值對t1可見
System.out.println(x);//x=10
});
x = 10;//1
t1.start();//2
}
}
- 線程終止原則(Thread Termination Rule):線程中所有操作happens-before於對此線程的終止檢測,我們可以通過Thread.join()等手段檢測到線程已經終止。如下示例中,因爲1 happens-before 2,而2又happens-before 3,所有t1中修改了x的值,對主線程可見
package com.zwx.happans.before;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class JoinRule {
static int x = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
x=100;//1
});
x = 10;
t1.start();
t1.join();//2
System.out.println(x);//3(x爲100,join之後t1修改了x後,x對主線程可見)
}
}
- 監視器鎖定規則(Monitor Lock Rule):一個unlock操作先行發生於後面對同一個鎖的lock操作。這裏必須強調的是必須爲同一個鎖,而“後面”是指的時間上的先後順序。如下示例,假如線程A先進去執行過一次x++,然後釋放鎖,然後線程B再進入同步代碼塊,那麼B獲得的x爲11:
package com.zwx.happans.before;
public class LockRule {
int x = 0;
public void demo(){
synchronized (this){//兩個線程同時訪問,相互修改的值均對對方可見
x++;
}
}
}
- 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,我們可以通過Thread.interrupted()方法檢測到是否有中斷髮生
- 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法
如果兩個操作之間的關係不在此列,並且無法從上面的關係中推導出來的話,它們就沒有順序性保障,虛擬機可以對他們隨意的進行重排序,但是不管怎麼隨意排序,正如前面所言,JMM其實是在遵循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序)編譯器和處理器怎麼優化都行。JMM這麼做的原因是:程序員對於這兩個操作是否真的被重排序並不關心,程序員關心的是程序執行時的語義不能被改變(即執行結果不能被改變)。因此,happens-before關係本質上和as-if-serial語義是一回事。
as-if-serial語義
as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
爲了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因爲這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作就可能被編譯器和處理器重排序。爲了具體說明,請看下面代碼示例:
package com.zwx.asifserial;
public class AsIfSerialDemo {
public static void main(String[] args) {
int a = 10;//1
int b = 10;//2
int c = a+ b;//3
}
}
上面示例中:1和3之間存在數據依賴關係,同時2和3之間也存在數據依賴關係。因此在最終執行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,程序的結果將會被改變)。但1和2之間沒有數據依賴關係,編譯器和處理器可以重排序1和2之間的執行順序。
總結
1、happens-before關係保證正確同步的多線程程序的執行結果不被改變。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。
2、as-if-serial語義保證單線程內程序的執行結果不被改變。as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。
as-if-serial語義和happens-before這麼做的目的,都是爲了在不改變程序執行結果的前提下,儘可能地提高程序執行的並行度。