天天看點

Happens-Before規則1. 前言2. 定義3. 再具體4. Happens-Before與JMM的關系5. 小結&參考資料

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的關系

Happens-Before規則1. 前言2. 定義3. 再具體4. Happens-Before與JMM的關系5. 小結&amp;參考資料

從圖可以看出:

  • JMM向程式員提供的happens-before規則能滿足程式員的需求。JMM的happens-before規則不但簡單易懂,而且也向程式員提供了足夠強的記憶體可見性保證(有些記憶體可見性保證其實并不一定真實存在,比如上面的A happens-before B)。
  • JMM對編譯器和處理器的束縛已經盡可能少。從上面的分析可以看出,JMM其實是在遵循一個基本原則:隻要不改變程式的執行結果(指的是單線程程式和正确同步的多線程程式),編譯器和處理器怎麼優化都行。例如,如果編譯器經過細緻的分析後,認定一個鎖隻會被單個線程通路,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個volatile變量隻會被單個線程通路,那麼編譯器可以把這個volatile變量當作一個普通變量來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率。
Happens-Before規則1. 前言2. 定義3. 再具體4. Happens-Before與JMM的關系5. 小結&amp;參考資料

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

5. 小結&參考資料

小結

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

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

參考資料

  • 《Java并發程式設計的藝術》
  • JSR-133:Java記憶體模型與線程規範(中文)
  • JSR-133: Java Memory Model and Thread Specification
  • Java記憶體模型以及happens-before規則
  • happens-before規則解析

繼續閱讀