java內存模型之:一文說清java線程的Happens-before

1. 前言

學習happens-before的目的不是隻限於知道這些規則的存在,而是要進一步知道如何實現和維護這些happens-before關係,在代碼中加以注意。html

Happens-before 規則是從java代碼設計層面保證有序性和可見性的機制。本文將會以圖示、樣例代碼和解釋相結合的方式,力圖闡述清楚happens-before的原理,爲理解如何保證線程安全性打下紮實的基礎。java

關於happens-before關係,java語言說明中有以下的描述:編程

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. -- Java Language Specification Ch 17.4.5. Happens-before Order

java代碼編譯成計算機指令後,計算機在執行指令時會進行必定程度的重排序。happens-before規則至關於java設計者和java開發者之間的約定,屏蔽了計算機底層的細節,從java層面和開發者創建約定,約定的內容是以下這樣:安全

java語言的設計者創建了一套happens-before規則,若是開發者在相應的場景下確保兩個操做之間聽從了happens-before規則,那java會爲開發者保證,聽從了happens-before規則的多線程操做具備實際的happens-before關係,進而能夠保證共享變量的可見性多線程

這些happenns-before關係大多須要開發者本身保證,而於此同時,java內置就已經實現了一系列happens-before關係,這些關happens-before 關係自然存在,而不須要開發者自行維護。併發

警戒誤區

網上絕大多數的文章在描述happens-before關係時,關於已經存在仍是須要開發者留心維護這個點上上沒有說清楚。
官方文檔所描述的happens-before並不是陳述句,不是在表述一個客觀事實,而是一個應該補上一個should,完整的語義應該爲: Action A should have a happens-before relationship with action B, in order to ensure result of action A is visible to action B.
若是咱們把須要咱們花氣力維護的happens-before關係當成了自然存在的關係,例如,這樣的一條規則:「* A write to a volatile field happens-before every subsequent read of that field.」,這是一條須要開發者維護的規則,若是開發者直接把這條規則當成了固有事實,不管怎樣的代碼順序均可以保證「對volatile變量的寫必定發生在讀以前」,那麼線程安全就徹底得不到保障了。
所以,後文對於happens-before介紹將主要分爲兩類:即已經存在開發者自行保證oracle

2. Happens-before規則

Happens-before關係可傳遞性

在學習下面的happens-before規則以前,咱們須要瞭解到的一個已有規則是,happens-before關係具備可傳遞性。用hb(a,b)表示a happens-before b,而若是 hb(b,c),則咱們能夠得知hb(a,c)。可傳遞性這一固有特性將幫助咱們順利理解後面的諸多規則。app

已經存在(須要瞭解)

這一部分happens-before規則已經由java語言設計者實現,故對於開發者而言它們是固有存在的事實,瞭解他們有助於咱們清楚代碼爲何能夠保證可見性,同時在複雜的狀況有分析可見性問題的能力。ide

1. 單線程規則

image.png
咱們,都知道,在單線程內部,對於開發者而言,能夠確保寫在前面的操做會happens-before以後的操做。儘管計算機底層的指令和java代碼的順序會有比較大的差別,但其中的細節jdk會去處理,java能夠向開發者保證,單線程全部前面代碼的操做結果對於後面代碼是可見的。對於這一條規則無需贅述,絕大多數開發者在編碼的第一天都是默認了這個事實。而這一規則也是其餘全部happens-before規則成立的基石。學習

2. Thread start規則

image.png
線程啓動規則指出,線程A中啓動線程B,那麼線程A中在調用ThreadB.start()方法happens-before線程B 中的全部操做。因爲可傳遞性的存在,咱們天然也能夠得知,A線程中調用ThreadB.start()以前的全部操做均happens-before B線程的全部操做,即A線程在調用ThreadB.start()以前的操做結果對B線程可見。

3. Thread join規則

image.png
該規則指出,線程B中的全部操做happens-before線程B的join()方法。同理,根據可傳遞性,咱們也能夠肯定,線程B中的全部操做如statement1 happens-before 線程A的statement2。

開發者自行保證(須要瞭解並清楚如何實現)

這一部分的規則須要開發者額外關注,由於java語言沒有內置機制保證這些happens-before規則,要實現這些場景下的可見性,須要開發者自行保證這一關係的存在。若是缺乏這些關係,則java沒法保證一個線程的操做結果對另外一個線程的可見性。

4. volatile變量規則

若是但願A線程的對volatile變量的修改對B線程可見,那麼A線程對volatile的變量的寫應該happens-before B線程對這個volatile變量的讀。

即若是保證線程A對一個volatile變量的寫發生在另外一個線程B對於這個變量的讀以前,在A對這個變量的操做對B可見。
下面經過一個簡單的代碼樣例驗證這一規則。

public class TestVolatileHappensBefore {
    static volatile int shared = 0;

    static class WriteTask implements Runnable {
        @Override
        public void run() {
            shared = 5; //1. write to the volatile shared variable
        }
    }

    static class ReadTaslk implements Runnable {
        @Override
        public void run() {
            System.out.println(shared);//2. read to the volatile shared variable
        }
    }

    public static void main (String[] args) {
        Thread tWrite = new Thread(new WriteTask());
        Thread tRead = new Thread(new ReadTaslk());
        tWrite.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            //just wait to ensure order
        }
        tRead.start();

    }
}

這段代碼的運行結果顯而易見,會打印shared的值爲5。由於聽從了volatile變量的happens-before規則,故註釋中1的操做結果對2可見。若是僅展現正例則沒法證實這一規則存在的必要性。而下面的這個反例則能夠證實咱們必須確保這一 happens-before 關係。

public static void main (String[] args) {
       Thread tWrite = new Thread(new WriteTask());
       Thread tRead = new Thread(new ReadTaslk());
       tRead.start();
       tWrite.start();

   }

反面例子中僅更改了對於volatile變量讀寫線程的開始順序,即咱們沒有嚴格遵照volatile變量的happens-before規則,那這樣的代碼運行結果會如何呢?這裏附上數次運行結果的截圖:
image.png

image.png
從結果能夠看出,在短短數次的重複運行中,shared變量的值有時爲初始值0,有時爲被寫進程修改後的值5。也就是更新後的shared變量的值沒法保證其對其餘線程的可見性。故若是須要保證volatile變量的可見性,咱們須要知足volatile變量的happens-before規則。

5. 鎖操做規則(synchronized及實現了Lock接口實現)

若是A線程是解鎖操做,以後另外一個B線程執行加鎖操做,若是但願A的操做結果對B可見,那麼 A應該happens-before B

一樣經過一段樣例代碼展現該規則的正面案例,依然是線程先讀後寫的狀況,同步機制依賴顯示鎖ReentrantLock:

public class TestLockHappensBefore {
    static int a = 0;
    static ReentrantLock lock = new ReentrantLock();

    public static void modify1() {
        lock.lock();
        try {
            System.out.println("a= " + a + " with " + Thread.currentThread().getName());
        } finally {
            a = 10;
            lock.unlock();
        }
    }

    public static void modify2() {
        lock.lock();
        try {
            System.out.println("a= " + a + " with " + Thread.currentThread().getName());
        } finally {
            a = 5;
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Thread thread0 = new Thread() {
            @Override
            public void run() {
                modify1();
            }
        };
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                modify2();
            }
        };

        thread0.start();
        thread1.start();
    }
}

thread0打印當前a的值並將a賦值爲10,thread1一樣打印當前a的值並將其賦值爲5。

由於thread0 符合happens-before thread1的規則,因此thread0對變量a的修改對thread1可見,故運行結果爲如圖:
image.png

而若是將啓動順序倒置

thread1.start();
thread0.start();

則關係變成了 thread1 happens-before thread0,則thread1對a的修改對thread0可見,運行結果如圖:
image.png
在這個樣例代碼中不存在關係的違反,但知足不一樣的happens-before關係,根據規則,則會有不一樣的結果,這須要開發者在開發過程當中留心觀察,理清邏輯。

6. 各式各樣的組合場景下的happens-before規則

這就是一個無窮無盡的話題了,在組合的場景下,只有符合了happens-before的線程兩兩之間可以保證可見性,而不必定能夠保證全部線程的互相可見性,要處理好這種狀況則須要開發者對
各類場景的happens-before規則爛熟於胸,利用好規則的可傳遞性,梳理清楚線程交互邏輯,纔有可能處理好全部線程間的可見性問題。

一個小彩蛋
筆者在撰寫樣例代碼的時候就遇到了這種組合的狀況,happens-before關係並無造成傳遞鏈,故出現了意想不到的結果,仔細分析才得了緣由,與各位簡單分享一下。代碼以下:

public class LockUnexpectedHappensBefore {
    static int a = 0;
    public static synchronized void read() {
        System.out.println(a);
    }
    public static synchronized void write() {
        a = 5;
    }

    public static void main(String[] args) {
        Thread write = new Thread(){
            public void run() {
                write();
            }
        };
        write.start();
        read();
    }
}

 看到這個代碼,不知道各位認爲打印出來的a的值會是多少,我一開始下意識認爲必定是5,而後結果是初始值0。
之因此會有這樣的結果,是由於儘管write 線程的start方法 happens-before 主線程調用read()方法,但由於建立線程和線程執行須要時間,write線程的run()方法並不被保證 happens-before 主線程的read()方法。
在這種狀況下write線程的unlcok不被保證 happens-before 於 主線程的lock,主線程天然無法看到write線程的操做結果。而若是咱們想要read的結果打印a = 5,方法也很簡單,運用咱們以前的幾個固有的happens-before規則。
例如,應用 thread join規則,將代碼改成:

write.start();
   write.join();
   read();

則write線程和主線程知足happens-before規則,具備實際的happens-before關係,在結合happens-before關係的傳遞性,happens-before關係從符合thread jion規則傳遞到符合鎖的規則,故write線程的修改必定對主線程可見。僅僅是這樣一個簡單的樣例尚且容易出錯,在更復雜的開發場景下,要想處理好可見性問題,只能靠開發人員本身不斷實踐和總結,才能獲得真知。

引用

Java Language Specification
java併發核心編程78講
Java - Understanding Happens-before relationship

相關文章
相關標籤/搜索