happens-before的定義
先行發生是Java內存模型中定義的兩項操作數之間餓的偏序關旭,如果操作A先行發生於操作B,其實就是在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發送了消息、調用了方法等。 先行發生是判斷是否存在競爭、線程是否安全的主要依據,依據這個原則,我們可以通過幾條規則一攬子解決併發環境下兩個操作之間是否可能存在衝突的所有問題。
特性
程序順序性規則
一個線程中的每個操作,happens-before於該線程中的任意後續操作。簡單來說就是,(多線程環境下)只要執行結果不被變更,無論怎麼“排序”都是對的。
volatile變量規則
對一個volatile域的寫,happens-before (先行發生)於任意後續對這個volatile域的讀
爲了解釋上面兩個規則,用下面程序進行說明
public class ReorderExample {
private int x = 0;
private int y = 1;
private volatile boolean flag = false;
public void writer(){
x = 42; //1
y = 50; //2
flag = true; //3
}
public void reader(){
if (flag){ //4
System.out.println("x:" + x); //5
System.out.println("y:" + y); //6
}
}
}
這裏涉及到了volatile的內存增強語意,先來看錶格
能否重排序 |
第二個操作 |
第二個操作 |
第二個操作 |
---|---|---|---|
第一個操作 |
普通讀/寫 |
volatile 讀 |
volatile 寫 |
普通讀/寫 |
- |
- |
NO |
volatile 讀 |
NO |
NO |
NO |
volatile 寫 |
- |
NO |
NO |
從這個表格最後一行,可以看出:
如果第二個操作爲volatile寫,不管第一個操作是什麼,都不能重排序,這就確保了“volatile寫之前的操作不會被重排序到volatile寫之後” ,拿上面的代碼來說,代碼1和代碼2不會被重排序到代碼3的後面,但是代碼1和2可能被重排序(沒有依賴也不會影響到執行結果),說到這裏和“程序順序性規則”是不是已經關聯起來了呢?
從表格的倒數第二行可以看出 :
如果一個操作爲volatile讀,不管第二個操作是什麼,都不能被重排序,這確保了volatile讀之後的操作都不會被重排序到volatile讀之前。拿上面的代碼來說,代碼4是讀取volatile變量,代碼5、6不會被重排序到代碼4之前。
傳遞性規則
如果A happens-before B,且B happens-before C ,那麼A happens-before C
從上面圖可以看出
-
x=42和y=50 happens-before flag=true ,這就是規則1
-
寫變量(代碼3) flag=true happens-before讀變量(代碼4) if(flag) ,這是規則2
根據規則3傳遞性規則,x=42 happens-before 讀變量if(flag)
謎案要揭曉了:
如果線程B讀到了flag是true,那麼x=43 和y=50對線程B就一定是可見了
通常而言,上面三個規則是一種聯合約束。
監視器鎖規則
對一個鎖的解鎖 happens-before於隨後對這個鎖的枷鎖
這個規則我覺得大家應該非常熟悉來,就是解釋synchronized關鍵字
public class SynchronizedExample {
private int x = 0;
public void synBlock(){
// 1.加鎖
synchronized (SynchronizedExample.class){
x = 1; // 對x賦值
}
// 3.解鎖
}
// 1.加鎖
public synchronized void synMethod(){
x = 2; // 對x賦值
}
// 3. 解鎖
}
先獲取鎖的線程,對x進行賦值之後再釋放鎖,另外一個線程再獲取鎖,一定能看到對x的賦值的改動,就是這麼簡單。可以用下面命令查看上面程序,看同步塊和同步方法轉換成還變指令有何不同!
javap -c -v SynchronizedExample
start規則
如果線程A執行操作ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操作 happens-before於線程B中的任何操作,也就是說:主線程A啓動來線程B後,子線程B能看到主線程再啓動子線程B前的操作。
public class StartExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
StartExample startExample = new StartExample();
Thread thread1 = new Thread(startExample::writer, "線程1");
startExample.x = 10;
startExample.y = 20;
startExample.flag = true;
thread1.start();
System.out.println("主線程結束");
}
public void writer(){
System.out.println("x:" + x );
System.out.println("y:" + y );
System.out.println("flag:" + flag );
}
}
運行結果:
主線程結束
x:10
y:20
flag:true
Process finished with exit code 0
線程1看到來主線程調用thread1.start()之前的所有賦值結果,這裏沒有打印【主線程結束】,這個和守護線程知識有關。
join()規則
如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回,和start規則剛好相反,主線程A等待子線程B完成,當子線程B完成後,主線程能夠看到子線程B的賦值操作。
public class JoinExample {
private int x = 0;
private int y = 1;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
JoinExample joinExample = new JoinExample();
Thread thread1 = new Thread(joinExample::writer, "線程1");
thread1.start();
thread1.join();
System.out.println("x:" + joinExample.x );
System.out.println("y:" + joinExample.y );
System.out.println("flag:" + joinExample.flag );
System.out.println("主線程結束");
}
public void writer(){
this.x = 100;
this.y = 200;
this.flag = true;
}
}
運行結果:
x:100
y:200
flag:true
主線程結束
Process finished with exit code 0
「主線程結束」這幾個字打印出來嘍,依舊和線程何時退出有關係
總結
-
Happens-before 重點是解決前一個操作結果對後一個操作可見,相信到這裏,你已經對Happens-before規則有所瞭解,這些規則解決來多線程編程的可見性於有序性問題,但還沒有完全解決原子性問題(除synchronized)
-
start 和join規則也是解決主線程與子線程通信的方式之一
-
從內存語意角度來說,volatile的【寫-讀】與鎖的【釋放-獲取】有相同效果;volatile寫和鎖的釋放有相同的內存語義;volatile讀與鎖的獲取有相同內存語義。volatile解決的是可見性問題,synchronized 解決的是原子性問題,兩則不應該混爲一談。
參考:https://juejin.im/post/5d80251d518825491b72419c