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