8大原則帶你秒懂Happens-Before原則

摘要:在並發編程中,Happens-Before原則是我們必須要掌握的,今天我們就一起來詳細聊聊並發編程中的Happens-Before原則。

本文分享自華為雲社區《【高並發】一文秒懂Happens-Before原則》,作者:冰 河。

在並發編程中,Happens-Before原則是我們必須要掌握的,今天我們就一起來詳細聊聊並發編程中的Happens-Before原則。

在正式介紹Happens-Before原則之前,我們先來看一段程式碼。

【示例一】

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //x的值是多少呢?
    }
  }
}

以上示例來源於://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong

這裡,假設執行緒A執行writer()方法,按照volatile會將v=true寫入記憶體;執行緒B執行reader()方法,按照volatile,執行緒B會從記憶體中讀取變數v,如果執行緒B讀取到的變數v為true,那麼,此時的變數x的值是多少呢??

這個示常式序給人的直覺就是x的值為42,其實,x的值具體是多少和JDK的版本有關,如果使用的JDK版本低於1.5,則x的值可能為42,也可能為0。如果使用1.5及1.5以上版本的JDK,則x的值就是42。

看到這個,就會有人提出問題了?這是為什麼呢?其實,答案就是在JDK1.5版本中的Java記憶體模型中引入了Happens-Before原則。

接下來,我們就結合案常式序來說明Java記憶體模型中的Happens-Before原則。

【原則一】程式次序規則

在一個執行緒中,按照程式碼的順序,前面的操作Happens-Before於後面的任意操作。

例如【示例一】中的程式x=42會在v=true之前執行。這個規則比較符合單執行緒的思維:在同一個執行緒中,程式在前面對某個變數的修改一定是對後續操作可見的。

【原則二】volatile變數規則

對一個volatile變數的寫操作,Happens-Before於後續對這個變數的讀操作。

也就是說,對一個使用了volatile變數的寫操作,先行發生於後面對這個變數的讀操作。這個需要大家重點理解。

【原則三】傳遞規則

如果A Happens-Before B,並且B Happens-Before C,則A Happens-Before C。

我們結合【原則一】、【原則二】和【原則三】再來看【示例一】程式,此時,我們可以得出如下結論:

(1)x = 42 Happens-Before 寫變數v = true,符合【原則一】程式次序規則。

(2)寫變數v = true Happens-Before 讀變數v = true,符合【原則二】volatile變數規則。

再根據【原則三】傳遞規則,我們可以得出結論:x = 42 Happens-Before 讀變數v=true。

也就是說,如果執行緒B讀取到了v=true,那麼,執行緒A設置的x = 42對執行緒B就是可見的。換句話說,就是此時的執行緒B能夠訪問到x=42。

其實,Java 1.5版本的 java.util.concurrent並發工具就是靠volatile語義來實現可見性的。

【原則四】鎖定規則

對一個鎖的解鎖操作 Happens-Before於後續對這個鎖的加鎖操作。

例如,下面的程式碼,在進入synchronized程式碼塊之前,會自動加鎖,在程式碼塊執行完畢後,會自動釋放鎖。

【示例二】

public class Test{
    private int x = 0;
    public void initX{
        synchronized(this){ //自動加鎖
            if(this.x < 10){
                this.x = 10;
            }
        } //自動釋放鎖
    }
}

我們可以這樣理解這段程式:假設變數x的值為10,執行緒A執行完synchronized程式碼塊之後將x變數的值修改為10,並釋放synchronized鎖。當執行緒B進入synchronized程式碼塊時,能夠獲取到執行緒A對x變數的寫操作,也就是說,執行緒B訪問到的x變數的值為10。

【原則五】執行緒啟動規則

如果執行緒A調用執行緒B的start()方法來啟動執行緒B,則start()操作Happens-Before於執行緒B中的任意操作。

我們也可以這樣理解執行緒啟動規則:執行緒A啟動執行緒B之後,執行緒B能夠看到執行緒A在啟動執行緒B之前的操作。

我們來看下面的程式碼。

【示例三】

//在執行緒A中初始化執行緒B
Thread threadB = new Thread(()->{
    //此處的變數x的值是多少呢?答案是100
});
//執行緒A在啟動執行緒B之前將共享變數x的值修改為100
x = 100;
//啟動執行緒B
threadB.start();

上述程式碼是在執行緒A中執行的一個程式碼片段,根據【原則五】執行緒的啟動規則,執行緒A啟動執行緒B之後,執行緒B能夠看到執行緒A在啟動執行緒B之前的操作,在執行緒B中訪問到的x變數的值為100。

【原則六】執行緒終結規則

執行緒A等待執行緒B完成(在執行緒A中調用執行緒B的join()方法實現),當執行緒B完成後(執行緒A調用執行緒B的join()方法返回),則執行緒A能夠訪問到執行緒B對共享變數的操作。

例如,在執行緒A中進行的如下操作。

【示例四】

Thread threadB = new Thread(()-{
    //在執行緒B中,將共享變數x的值修改為100
    x = 100;
});
//在執行緒A中啟動執行緒B
threadB.start();
//在執行緒A中等待執行緒B執行完成
threadB.join();
//此處訪問共享變數x的值為100

【原則七】執行緒中斷規則

對執行緒interrupt()方法的調用Happens-Before於被中斷執行緒的程式碼檢測到中斷事件的發生。

例如,下面的程式程式碼。在執行緒A中中斷執行緒B之前,將共享變數x的值修改為100,則當執行緒B檢測到中斷事件時,訪問到的x變數的值為100。

【示例五】

 //在執行緒A中將x變數的值初始化為0
    private int x = 0;

    public void execute(){
        //在執行緒A中初始化執行緒B
        Thread threadB = new Thread(()->{
            //執行緒B檢測自己是否被中斷
            if (Thread.currentThread().isInterrupted()){
                //如果執行緒B被中斷,則此時X的值為100
                System.out.println(x);
            }
        });
        //在執行緒A中啟動執行緒B
        threadB.start();
        //在執行緒A中將共享變數X的值修改為100
        x = 100;
        //在執行緒A中中斷執行緒B
        threadB.interrupt();
    }

【原則八】對象終結原則

一個對象的初始化完成Happens-Before於它的finalize()方法的開始。

例如,下面的程式程式碼。

【示例六】

public class TestThread {

   public TestThread(){
       System.out.println("構造方法");
   }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("對象銷毀");
    }

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

運行結果如下所示。

構造方法
對象銷毀

好了,今天就到這兒吧。我們下期見~~

 

點擊關注,第一時間了解華為雲新鮮技術~