摘要:本文我們就結合案例程式來說明Java記憶體模型中的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(); } }
運行結果如下所示,
構造方法
物件銷毀
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/540601.html
標籤:其他
上一篇:面向物件與面向程序
