天天看點

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結

原創聲明:本文來自公衆号【胖滾豬學程式設計】,以漫畫形式讓程式設計so easy and interesting,轉載請注明出處!

在上一篇文章

并發程式設計三大源頭

中,我們初識了并發程式設計的三個bug源頭:可見性、原子性、有序性。明白了它們究竟為什麼會發生,那麼今天我們就來聊聊如何解決這三個問題吧。

序幕

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結

Happens-Before是什麼?

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結
A Happens-Before B 意味着 A 事件對 B 事件來說是可見的,無論 A 事件和 B 事件是否發生在同一個線程裡。例如 A 事件發生線上程 1 上,B 事件發生線上程 2 上,Happens-Before 規則保證線程 2 上也能看到 A 事件的發生。
【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結

Happens-Before的作用

happens-before原則非常重要,它是判斷線程是否安全的主要依據,依靠這個原則,我們就能解決在并發環境下可見性和有序性問題。

比如某天老闆問你“胖滾豬,我這段并發代碼會有線程安全問題嗎”,那麼你可以對照着happens-before原則一個個看,要是符合其中之一并且是原子性的,你就可以大聲告訴老闆“沒得問題!”

比如這段代碼:

i = 1;       //線程A執行
j = i ;      //線程B執行           

j 是否等于1呢?假定線程A的操作(i = 1)happens-before線程B的操作(j = i),那麼可以确定線程B執行後j = 1 一定成立,如果他們不存在happens-before原則,那麼j = 1 不一定成立。

這就是happens-before原則的威力!讓我們走進它的世界吧!

Happens-Before八大原則 解決原子性和有序性問題

規則一:程式的順序性規則

這條規則是指在一個線程中,按照程式順序,前面的操作 Happens-Before 于後續的任意操作。這規則挺好了解的,畢竟是在一個線程中呐。

你會覺得這是個廢物規則。其實這個規則是一個基礎規則,happens-before 是多線程的規則,是以要和其他規則限制在一起才能展現出它的順序性,别着急,繼續向下看。

規則二: Volatile變量規則

這條規則是指對一個 volatile 變量的寫操作, Happens-Before 于後續對這個 volatile 變量的讀操作。我們在上篇文章說過,因為緩存的原因,每個線程有自己的工作記憶體,如果共享變量沒有及時刷到主記憶體中,那就會導緻可見性問題,線程B沒有及時讀到線程A的寫。但是隻要加上Volatile,就可以避免這個問題,相當于volatile的作用是對變量的修改會繞過高速緩存立刻重新整理到主存。不過要注意一下,volatile除了保證可用性,它還可以禁止指定重排序哦!

public class TestVolatile1 {
    private volatile static int count = 0;
    public static void main(String[] args) throws Exception {
        final TestVolatile1 test = new TestVolatile1();
        Thread th1 = new Thread(() -> {
            count = 10;
        });
        Thread th2 = new Thread(() -> {
            //沒有volatile修飾count的話極小機率會出現等于0的情況
            System.out.println("count=" + count);
        });
        // 啟動兩個線程
        th1.start();
        th2.start();
    }
}           

規則三: 傳遞性規則

這條規則是指如果 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。這也很好了解。我們舉個例子,writer和reader是兩個不同的線程,它們有如下操作:

int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42; //(1)
    v = true; //(2)
  }
  public void reader() {
    if (v == true) { //(3)
      // 這裡 x 會是多少呢?(4)
    }
  }           

這個例子和上面那個Volatile的例子有個差別就是,有兩個變量。那麼我們來分析一下:

(1)和(2)在同一個線程中,根據規則1,(1)Happens-Before于(2)

(3)和(4)在同一個線程中,同理,(3)Happens-Before于(4)

根據規則2,由于v用了volatile修飾,那麼(2)必然 Happens-Before于(3)。

那麼根據傳遞性規則可得:(1)Happens-Before于(4),是以x必然為42。

是以即使x沒有用volatile,它也是可以保證可見性的!是以為啥剛剛說規則1要和其他規則聯合起來看才有意思,現在你知道了吧!

規則四: 管程中的鎖規則

指管程中的解鎖必然發生在随後的加鎖之前。管程是一種通用的同步原語,synchronized 是 Java 裡對管程的實作。管程中的鎖在 Java 裡是隐式實作的,例如下面的代碼,在進入同步塊之前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫我們實作的。

synchronized (this) { // 此處自動加鎖
  if (this.x < 10) {//臨界區
  }  
} // 此處自動解鎖           

這個規則比較好了解,無論是在單線程環境還是多線程環境,一個鎖處于被鎖定狀态,那麼必須先執行unlock操作後面才能進行lock操作。

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結

規則五: 線程啟動規則

主線程 A 啟動子線程 B 後(線程 A 調用線程 B 的 start() 方法),子線程 B 能夠看到主線程在啟動子線程 B 前的操作。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        // 主線程調用 B.start() 之前 所有對共享變量的修改,此處皆可見
        // 是以count肯定為10
        System.out.println(count);
    });
    // 此處對共享變量count修改
    count = 10;
    // 主線程啟動子線程
    B.start();
}           

規則六: 線程終止規則

主線程 A 等待子線程 B 完成(主線程 A 通過調用子線程 B 的 join() 方法實作),如果線上程 A 中,調用線程 B 的 join() 并成功傳回,那麼主線程能夠看到子線程的操作(指共享變量的操作),換句話說就是線程 B 中的任意操作 Happens-Before 于該 join() 操作的傳回。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        // 主線程調用 B.start() 之前 所有對共享變量的修改,此處皆可見
        // 是以count肯定為10
        count = 10;
    });

    // 主線程啟動子線程
    B.start();
    // 主線程等待子線程完成
    B.join();
    // 子線程所有對共享變量的修改 在主線程調用 B.join() 之後皆可見
    System.out.println(count);//count必然為10
}           

規則七:線程中斷規則

對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生。即線程A調用線程B的interrupt()方法,happens-before于線程A發現B被A中斷(通過Thread.interrupted()方法檢測到是否有中斷發生)。

private static long acount = 0;
private static long bcount = 0;
public static void main(String[] args) throws InterruptedException {
    Thread B = new Thread(() -> {
        bcount = 7;
        System.out.println("Thread A被中斷前bcount="+bcount+" acount="+acount);
        while (true){
            if (Thread.currentThread().isInterrupted()){
                bcount = 77;
                System.out.println("Thread A被中斷後bcount="+bcount+" acount="+acount);
                return;
            }
        }
    });
    B.start();
    Thread A = new Thread(() -> {
        acount = 10;
        System.out.println("Thread B 中斷A前bcount="+bcount+" acount="+acount);
        B.interrupt();
        acount = 100;
        System.out.println("Thread B 中斷A後bcount="+bcount+" acount="+acount);
    });
    A.start();
}           

規則八:對象規則

一個對象的初始化完成(構造函數執行結束,一般都是用new初始化)happen—before它的finalize()方法的開始。finalize()是在java.lang.Object裡定義的,即每一個對象都有這麼個方法。這個方法在該對象被回收的時候被調用。該條原則強調的是多線程情況下對象初始化的結果必須對發生于其後的對象銷毀方法可見。

public HappensBefore8(){
        System.out.println("構造方法");
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("對象銷毀");
    }

    public static void main(String[] args){
        new HappensBefore8();
        System.gc();
    }           

關于有序性的那些疑問

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結

擴充有序性的概念:Java記憶體模型中的程式天然有序性可以總結為一句話,如果在本線程内觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程内表現為串行語義”,後半句是指“指令重排序”現象和“工作記憶體主主記憶體同步延遲”現象。 這其實還涉及到一個高頻面試考點:as-if-serial語義

as-if-serial語義:不管怎麼重排序,單線程程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。是以編譯器和處理器不會對存在資料依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關系,這些操作就可能被編譯器和處理器重排序。

劃重點:單線程中保證按照順序執行。

synchronized同一時刻隻有一個線程在運作,也就相當于保證了有序性。至于這個雙重檢查案例,出問題,并不是因為synchronized沒有保證有序性。而是指令重排導緻了在多個線程中無序。

總結

【漫畫】JAVA并發程式設計 如何解決可見性和有序性問題序幕Happens-Before是什麼?關于有序性的那些疑問總結