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(); } }
運行結果如下所示。
構造方法
對象銷毀
好了,今天就到這兒吧。我們下期見~~