Java並發(2)- 聊聊happens-before


引言

上一篇文章聊到了Java內存模型,在其中我們說JMM是建立在happens-before(先行發生)原則之上的。
為什么這么說呢?因為在Java程序的執行過程中,編譯器和處理器對我們所寫的代碼進行了一系列的優化來提高程序的執行效率。這其中就包括對指令的“重排序”。
重排序導致了我們代碼並不會按照代碼編寫順序來執行,那為什么我們在程序執行后結果沒有發生錯亂,原因就是Java內存模型遵循happens-before原則。在happens-before規則下,不管程序怎么重排序,執行結果不會發生變化,所以我們不會看到程序結果錯亂。

重排序

重排序是什么?通俗點說就是編譯器和處理器為了優化程序執行性能對指令的執行順序做了一定修改。
重排序會發生在程序執行的各個階段,包括編譯器沖排序、指令級並行沖排序和內存系統重排序。這里不具體分析每個重排序的過程,只要知道重排序導致我們的代碼並不會按照我們編寫的順序來執行。
在單線程的的執行過程中發生重排序后我們是無法感知的,如下代碼所示,

int a = 1;  //步驟1
int b = 2;  //步驟2
int c = a + b; //步驟3 

1和2做了重排序並不會影響程序的執行結果,在某些情況下為了優化性能可能會對1和2做重排序。2和3的重排序會影響執行結果,所以編譯器和處理器不會對2和3進行重排序。
在多線程中如果沒有進行正確的同步,發生重排序我們是可以感知的,比如下面的代碼:

public class AAndB {

	int x = 0;
	int y = 0;
	int a = 0;
	int b = 0;
	
	public void awrite() {

		a = 1;
		x = b;
	}
	
	public void bwrite() {

		b = 1;
		y = a;
	}
}

public class AThread extends Thread{

	private AAndB aAndB;
	
	public AThread(AAndB aAndB) {
		
		this.aAndB = aAndB;
	}
	
	@Override
	public void run() {
		super.run();
		
		this.aAndB.awrite();
	}
}

public class BThread extends Thread{

	private AAndB aAndB;
	
	public BThread(AAndB aAndB) {
		
		this.aAndB = aAndB;
	}
	
	@Override
	public void run() {
		super.run();
		
		this.aAndB.bwrite();
	}
}

private static void testReSort() throws InterruptedException {

	AAndB aAndB = new AAndB();

	for (int i = 0; i < 10000; i++) {
		AThread aThread = new AThread(aAndB);
		BThread bThread = new BThread(aAndB);

		aThread.start();
		bThread.start();

		aThread.join();
		bThread.join();

		if (aAndB.x == 0 && aAndB.y == 0) {
			System.out.println("resort");
		}

		aAndB.x = aAndB.y = aAndB.a = aAndB.b = 0;

	}

	System.out.println("end");
}

如果不進行重排序,程序的執行順序有四種可能:




但程序在執行多次后會打印出“resort”,這種情況就說明了A線程和B線程都出現了重排序。

happens-before的定義

happens-before定義了八條規則,這八條規則都是用來保證如果A happens-before B,那么A的執行結果對B可見且A的執行順序排在B之前。

  1. 程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)后執行的操作。
  2. 管理鎖定規則:一個unlock操作happen—before后面(時間上的先后順序,下同)對同一個鎖的lock操作。
  3. volatile變量規則:對一個volatile變量的寫操作happen—before后面對該變量的讀操作。
  4. 線程啟動規則:Thread對象的start()方法happen—before此線程的每一個動作。
  5. 線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
  6. 線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。
  7. 對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
  8. 傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

happens-before定義了這么多規則,其實總結起來可以歸納為一句話:happens-before規則保證了單線程和正確同步的多線程的執行結果不會被改變。
那為什么有程序次序規則的保證,上面多線程執行過程中還是出現了重排序呢?這是因為happens-before規則僅僅是java內存模型向程序員做出的保證。在單線程下,他並不關心程序的執行順序,只保證單線程下程序的執行結果一定是正確的,java內存模型允許編譯器和處理器在happens-before規則下對程序的執行做重排序。
而且從程序員角度來說,對於兩個操作是否真的被重排序並不關心,關心的是程序執行結果是否被改變。
上面的程序在單線程會被重排序的情況下又沒有對多線程同步,這樣就導致了意料之外的結果。

as-if-serial語義

《Java並發編程的藝術》中解釋:

as-if-serial就是不管怎么重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。

這句話通俗理解就是as-if-serial語義保證單線程程序的執行結果不會被改變。
本質上和happens-before規則是一個意思:happens-before規則保證了單線程和正確同步的多線程的執行結果不會被改變。都是對執行結果做保證,對執行過程不做保證。
這也是JMM設計上的一個亮點:既保證了程序員編程時的方便以及正確,又同時保證了編譯器和處理器更大限度的優化自由。


參考資料:
《深入理解Java內存模型》
《深入理解Java虛擬機》
《Java並發編程的藝術》


免責聲明!

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



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