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

knock_小新發表於2018-07-18

引言

上一篇文章聊到了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");
}
複製程式碼

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

Java併發(2)- 聊聊happens-before
Java併發(2)- 聊聊happens-before
Java併發(2)- 聊聊happens-before
Java併發(2)- 聊聊happens-before
但程式在執行多次後會列印出“resort”,這種情況就說明了A執行緒和B執行緒都出現了重排序。
Java併發(2)- 聊聊happens-before

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併發程式設計的藝術》

相關文章