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的值是多少呢?
    }
  }
}

以上示例來源於:http://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();
    }
}

運行結果如下所示。

構造方法
對象銷毀

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

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM