1. 程式人生 > 程式設計 >Happens-Before規則

Happens-Before規則

Happens-Before規則

1. 前言

從 JDK 5開始,Java 使用新的 JSR-133 記憶體模型,使用 happens-before 的概念來闡述操作間的可見性。

2. 定義

JSR-133 對Happens-Before 的定義:

Happens-Before Relationship Two actions can be ordered by a happens-before relationship. If one action > happens-before another,then the first is visible to and ordered before the second. It should be stressed that a happens-before relationship between two actions does not imply that those actions must occur in that order in a Java platform implementation. The happens-before relation mostly stresses orderings between two actions that conflict with each other,and defines when data races take place. There are a number of ways to induce a happens-before ordering,including:

  • Each action in a thread happens-before every subsequent action in that thread.
  • An unlock on a monitor happens-before every subsequent lock on that monitor.
  • A write to a volatile field happens-before every subsequent read of that volatile.
  • A call to start() on a thread happens-before any actions in the started thread.
  • All actions in a thread happen-before any other thread successfully returns from a join() on that thread.
  • If an action a happens-before an action b,and b happens before an action c,then a happensbefore c.

定義: 如果一個操作happens-before另一個操作,那麼意味著第一個操作的結果對第二個操作可見,而且第一個操作的執行順序將排在第二個操作的前面。 兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須按照happens-before關係指定的順序來執行。如果重排序之後的結果,與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。具體規則如下:

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  • volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  • 執行緒啟動規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
  • 執行緒終結規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before於執行緒A從ThreadB.join()操作成功返回。
  • 傳遞性規則:如果A happens-before B,且B happens-before C,那麼A happens-before C。

注:說明一下,網上搜出來有的是8條規則,我不知道還有兩條哪兒來的,JSR-133 裡面只有這六條。網上的還有下面兩條:

  • 執行緒中斷操作:對執行緒interrupt()方法的呼叫,happens-before於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到執行緒是否有中斷髮生。
  • 物件終結規則:一個物件的初始化完成,happens-before於這個物件的finalize()方法的開始。

3. 再具體

程式順序規則:一段程式碼在單執行緒中執行的結果是有序的。注意是執行結果,因為虛擬機器器、處理器會對指令進行重排序。雖然重排序了,但是並不會影響程式的執行結果,所以程式最終執行的結果與順序執行的結果是一致的。故而這個規則只對單執行緒有效,在多執行緒環境下無法保證正確性。

監視器鎖規則:這個規則比較好理解,無論是在單執行緒環境還是多執行緒環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操作後面才能進行lock操作。

volatile變數規則:這是一條比較重要的規則,它標誌著volatile保證了執行緒可見性。通俗點講就是如果一個執行緒先去寫一個volatile變數,然後一個執行緒去讀這個變數,那麼這個寫操作一定是happens-before讀操作的。

執行緒啟動規則:假定執行緒A在執行過程中,通過執行ThreadB.start()來啟動執行緒B,那麼執行緒A對共享變數的修改在接下來執行緒B開始執行後確保對執行緒B可見。

執行緒終結規則:假定執行緒A在執行的過程中,通過制定ThreadB.join()等待執行緒B終止,那麼執行緒B在終止之前對共享變數的修改線上程A等待返回後可見。

傳遞性規則:提現了happens-before原則具有傳遞性。

特別強調happens-hefore不能理解為“時間上的先後順序”。 我們來看如下程式碼:

public class VolatileTest {
    private int a = 0;
    private int getA() {
        return a;
    }
    private void setA(int a) {
        this.a = a;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            VolatileTest volatileTest = new VolatileTest();
            Thread thread1 = new Thread(() -> {
                volatileTest.setA(10);
            });
            thread1.start();

            Thread thread2 = new Thread(() -> {
                System.out.print(volatileTest.getA()+" ");
            });
            thread2.start();
        }
    }
}
複製程式碼

上面程式碼就是一組簡單的setter/getter方法,現在假設現在有兩個執行緒 thread1 和 thread2,執行緒 thread1 先(這裡指時間上的先執行)執行setA(10),然後執行緒 thread2 訪問同一個物件的getA()方法,那麼此時執行緒B收到的返回值是對少呢?

答案:不確定

0 0 0 0 10 0 10 10 10 0 10 0 10 10 10 10 10 0 10 10 0 0 0 10 0 10 10 10 0 10 0 10 10 10 0 10 10 0 10 10 10 0 0 10 10 0 10 0 10 10 10 10 10 10 10 10 10 10 0 0 0 10 10 0 10 0 10 0 0 0 10 10 0 10 10 10 10 10 10 10 10 10 10 10 0 10 10 10 0 10 10 10 10 10 0 10 0 10 0 0 
複製程式碼

雖然執行緒 thread1 在時間上先於執行緒 thread2 執行,但是由於程式碼完全不適用happens-before規則,因此我們無法確定先 thread2 收到的值時多少。也就是說上面程式碼是執行緒不安全的。

4. Happens-Before與JMM的關係

JMM的設計圖

從圖可以看出:

  • JMM向程式設計師提供的happens-before規則能滿足程式設計師的需求。JMM的happens-before規則不但簡單易懂,而且也向程式設計師提供了足夠強的記憶體可見性保證(有些記憶體可見性保證其實並不一定真實存在,比如上面的A happens-before B)。
  • JMM對編譯器和處理器的束縛已經儘可能少。從上面的分析可以看出,JMM其實是在遵循一個基本原則:只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。例如,如果編譯器經過細緻的分析後,認定一個鎖只會被單個執行緒訪問,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個volatile變數只會被單個執行緒訪問,那麼編譯器可以把這個volatile變數當作一個普通變數來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率。

happens-before與JMM的關係

一個happens-before規則對應於一個或多個編譯器和處理器重排序規則。對於Java程式設計師來說,happens-before規則簡單易懂,它避免Java程式設計師為了理解JMM提供的記憶體可見性保證而去學習複雜的重排序規則以及這些規則的具體實現方法.

5. 小結&參考資料

小結

時間先後順序與happens-before原則之間基本沒有太大的關係,所以我們在衡量併發安全問題的時候不要受到時間順序的幹擾,一切必須以happens-before原則為準。

簡單的說,happens-before 規則就是為了讓程式猿更好的理解 JMM 提供的記憶體可見性而編寫的規則,讓程式猿能避免去學習編譯器和底層編譯原理的重排序規則。

參考資料