【併發編程系列4】JMM中happens-before規則和as-if-serial語義 先行發生原則(happens-before) as-if-serial語義 總結

先行發生原則(happens-before)

我們知道,在Java內存模型中,如果要確保有序性可以靠volatilesynchronized來實現,但是如果所有的有序性都僅僅依靠這兩個關鍵字來完成,那麼有一些操作將會變得很繁瑣,但是我們在編寫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虛擬機》一書,結合自己的理解部分規則給出了示例):

  1. 程序次序規則(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
        }
    }
}
  1. volatile 變量規則(Volatile Lock Rule):對於 volatile 修飾的變量的寫的操作,一定 happen-before 後續對於volatile變量的讀操作。如上順序規則示例中,因爲變量flag加了volatile修飾,所以2 happens-before 3
  2. 傳遞性規則(Transitivity Rule):如上順序規則示例中,根據順序規則:1 happens-before 2;根據volatile變量規則:2 happens-before 3;故而根據傳遞性規則可以推導出:1 happens-before 3。
  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
    }
}
  1. 線程終止原則(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對主線程可見)
    }
}
  1. 監視器鎖定規則(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++;
        }
    }
}
  1. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,我們可以通過Thread.interrupted()方法檢測到是否有中斷髮生
  2. 對象終結規則(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這麼做的目的,都是爲了在不改變程序執行結果的前提下,儘可能地提高程序執行的並行度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章