併發編程之happens-before

前言
Jdk5開始,Java使用新的JSP-133內存模型,JSR-133使用happens-before的概念來闡述操作直接的內存可見性,那麼這兩個操作之間必須要存在happen-before關係。

一、文章導圖

導圖.png

二、Happens-Before

happens-before是JMM的核心概念,要理解happens-before,先來看下JMM(Java Memory Model)的設計意圖。

1、JMM的設計意圖

設計JMM,需要考慮的兩個關鍵因素:
1)、程序員對內存模型的使用。程序員希望內存模型易於理解、易於編程,希望一個強內存模型來編寫代碼。
2)、編譯器和處理器對內存模型的實現。編譯器和處理器希望內存模型對它們的束縛越少越好,這樣它們就可能做更多的優化來提高性能,希望實現一個若內存模型。

兩個因素相互矛盾,如何找到一個平衡點呢?

設計JMM的核心目的:
一方面,爲程序員提供足夠強的內存可見性保證;另一方面,對編譯器和處理器的限制儘可能放鬆。

設計JMM的策略:對於一段程序
1)對於會改變程序的執行結果的重排序,JMM要求編譯器和處理器禁止這種排序
2)對於不會改變程序的執行結果的重排序,JMM對編譯器和處理器不做要求

JMM設計示意圖.jpeg

2、Happens-before介紹

《JSR-133:Java Memory Model and Thread Specification》對happens-before關係的定義如下:
1)、如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見。
2)、兩個操作之間存在happens-before關係,如果重排序後的執行結果與按happens-before關係指定的順序執行結果一致,這種重排序是被允許的。
上面第1條是對程序員的承諾,第2條是對編譯器和處理器重排序的約束原則。

JMM遵循的的一個原則是:只要不改變程序的執行結果,編譯器和處理器怎麼優化都可以。JMM這麼做的原因是程序員對於是否進行重排序等並不關心,關係的是執行結果。

3、 Happens-before規則

《JSR-133:Java Memory Model and Thread Specification》定義瞭如下的happens-before原則。
1)、程序順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。
2)、監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)、volatile變量規則:對於一個volatile域的寫,happens-before於任意後續對這個變量的讀。
4)、傳遞性:如果A happens-before B,且 B happens-before C,那麼A happens-before C。
5)、start()規則:如果線程A 執行操作 ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操作happens-before於線程B中的任意操作。
6)、join()規則:如果線程A 執行操作ThreadB.join()併成功,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。

如下示例說明:
happens-before示例.jpeg
如圖:按程序順序規則知,1 happens-before 2;3 happens-before 4;按volatile變量規則知,2 happens-before 3;再有傳遞性可知1 happens-before 4。

那麼,什麼是重排序呢?爲什麼要重排序呢?

三、重排序

重排序:是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的手段。

處理器爲啥要重排序呢?
因爲一條指令可能會涉及到很多步驟,而每個步驟可能會用到不同的寄存器。CPU使用了流水先的方式進行處理,CPU有多個功能單元(如獲取、解碼、運算等),一條指令也分爲多個單元,那麼第一條指令執行還沒完畢,就有可能執行第二條指令,前提是這兩條指令功能單元相同或相似,所以可以通過重排序的方式使得功能單元相似的指令連接執行,來減少流水線中斷的情況。
比如說:
執行方式1

int x = 1;
int y = 2;
x = x + 1;
y = y + 1;

執行方式2

int x = 1;
x = x + 1;
int y = 2;
y = y + 1;

性能方面:執行方式2可能比執行方式1好點,因爲執行方式2中x或y已經在寄存器中了,獲取和運算會連續執行。

四、鎖獲取-釋放建立的happens-before關係

1、happens-before關係

鎖是Java併發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程想獲取同一個鎖的線程發送消息。
如下鎖釋放-獲取的示例代碼:
`public class MonitorExample {

int a = 0;

public synchronized void writer() { // 1
    a++;                            // 2
}                                   // 3

private synchronized void reader() {   // 4
    int i = a;                         // 5
    System.out.println(i);             // 6
}

}
`
如果線程A執行writer()方法,隨後線程B執行reader()方法。其包含的happens-before規則有:
1)、程序順序執行:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
2)、根據監視器鎖規則:3 happens-before 4。
3)、結合傳遞性:2 happens-before 5。
因此,線程A在釋放鎖之前的對所以共享變量的操作,在線程B獲取該鎖後對共享變量立即可見。

2、內存語義

如上示例,當線程A釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中;當線程B獲得鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得監視器保護的臨界區代碼必須從主內存讀取共享變量。如下圖所示:
鎖獲取示意圖.png
其內存語義可理解:

  • 線程A 釋放一個鎖,實質上是線程A向接下來的獲取該鎖的其它線程發送一個消息(對共享變量做了修改)
  • 線程B 獲取一個鎖,實質上是線程B接收了之前某個線程發出的消息(已對共享變量做了修改)
  • 線程A釋放鎖,隨後線程B獲取該鎖,這個過程實質上是線程A通過主內存向線程B發送消息。
鎖內存語義的實現依賴於Java同步器框架 AbstractQueuedSynchronizer(AQS),其它篇幅再做詳細介紹。

五、總結

主要介紹JMM內存模型的設計意圖,第一爲了滿足程序員的對代碼的易於理解、易於編程;同時內存模型對編譯器和處理器的束縛越少越好,這樣它們就可能做更多的優化來提高性能。
另外介紹了對happens-before的認識及其規則,在具體場景如果體現這種happens-before關係。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章