高併發學習之06關鍵字volatile

	本文參考《Java併發編程的藝術》

1. volatile 簡介

在上一篇文章中我們瞭解了synchronized關鍵字,並瞭解併發編程JMM中synchronized是怎麼保證原子性、可見性、有序性。在這裏在囉嗦下,在 Java 併發編程中,要想使併發程序能夠正確地執行,必須要保證三條原則,即:原子性、可見性和有序性。只要有一條原則沒有被保證,就有可能會導致程序運行不正確。
上篇中我們瞭解了synchronized原理,知道synchronized有鎖升級的機制,但是不管怎麼synchronized都是java中重量級的數據同步機制。這一篇我們就詳細瞭解下關鍵字voldatile.
volatile 是java提供的一種最輕量級的同步機制。volatile關鍵字被用來保證可見性,可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。

2. volatile原理

volatile變量修飾的共享變量,在進行寫操作的時候會多出一個lock前綴的彙編指令,這個指令在前面我們講解CPU高速緩存的時候提到過,會觸發總線鎖或者緩存鎖,通過緩存一致性協議來解決可見性問題。
對於聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,把這個變量所在的緩存行的數據寫回到系統內存,再根據我們前面提到過的MESI的緩存一致性協議,來保證多CPU下的各個高速緩存中的數據的一致性。
簡單的說volatile實現了三點:

  • Lock前綴的指令會引起處理器緩存寫回內存
  • 一個處理器的緩存回寫到內存會導致其他處理器的緩存失效
  • 處理器發現本地緩存失效後,就會從內存中重讀該變量數據,即可以獲取當前最新值。

那麼在JMM中的理解其實就是:關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。

3. 簡單瞭解下JMM中定義的happens-before規則

JSR-133(JDK 5開始)使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個線程之內,也可以是在不同線程之間。因此,JMM可以通過happens-before關係向程序員提供跨線程的內存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關係,儘管a操作和b操作在不同的線程中執行,但JMM向程序員保證a操作將對b操作可見)。具體的定義爲:

  • 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

  • 兩個操作之間存在happens-before關係,並不意味着Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。

上面的第一點是JMM對程序員的承諾。

從程序員的角度來說,可以這樣理解happens-before關係:如果A happens-before B,那麼Java內存模型將向程序員保證——A操作的結果將對B可見,且A的執行順序排在B之前。注意,這只是JMM向程序員做出的保證!

上面的第二點是JMM對編譯器和處理器重排序的約束原則。這裏我們就不深入了。

4. volatile寫-讀建立的happens-before關係

從內存語義的角度來說,volatile的寫-讀與鎖的釋放-獲取有相同的內存效果:volatile寫和鎖的釋放有相同的內存語義;volatile讀與鎖的獲取有相同的內存語義。

class VolatileExample {
	int a = 0;
	volatile boolean flag = false;
	public void writer() {
		a = 1;     // 1
		flag = true;   // 2
	}
	public void reader() {
		if (flag) {    // 3
		int i = a;  // 4
		……
		}
   }
}

假設線程A執行writer()方法之後,線程B執行reader()方法。根據happens-before規則,這個過程建立的happens-before關係可以分爲3類:
1)根據程序次序規則,1 happens-before 2; 3 happens-before 4。
2)根據volatile規則,2 happens-before 3。
3)根據happens-before的傳遞性規則,1 happens-before 4。
上述happens-before關係的圖形化表現形式如下:
volatile線程間規則
圖中每一個箭頭兩個節點就代碼一個happens-before關係,黑色的代表根據程序順序規則推導出來,紅色的是根據volatile變量的寫happens-before 於任意後續對volatile變量的讀,而藍色的就是根據傳遞性規則推導出來的。這裏的2 happen-before 3,同樣根據happens-before規則定義:如果A happens-before B,則A的執行結果對B可見,並且A的執行順序先於B的執行順序,我們可以知道操作2執行結果對操作3來說是可見的,也就是說當線程A將volatile變量 flag更改爲true後線程B就能夠迅速感知。

5.volatile寫-讀的內存語義

volatile寫的內存語義

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。

volatile讀的內存語義

  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

對於寫的理解,我們已上面示例程序VolatileExample爲例,假設線程A首先執行writer()方法,隨後線程B執行reader()方法,初始時兩個線程的本地內存中的flag和a都是初始狀態。下圖是線程A執行volatile寫後,共享變量的狀態示意圖。
共享變量的狀態
線程A在寫flag變量後,本地內存A中被線程A更新過的兩個共享變量的值
被刷新到主內存中。此時,本地內存A和主內存中的共享變量的值是一致的。

對於讀的內存語義理解爲:
讀的內存語義
如圖所示,在讀flag變量後,本地內存B包含的值已經被置爲無效。此時,線程B必須從主內存中讀取共享變量。線程B的讀取操作將導致本地內存B與主內存中的共享變量的值變成一致。
如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀線程B讀一個volatile變量後,寫線程A在寫這個volatile變量之前所有可見的共享變量的值都將立即變得對讀線程B可見。

6.volatile內存語義的實現

6.1 指令重排序

JMM內存模型提到過重排序分爲編譯器重排序和處理器重排序。爲了實現volatile內存語義,JMM會分別限制這兩種類型的重排序類型。下表是JMM針對編譯器制定的volatile重排序規則表。
volatile重排序規則表
通過上表可以看出:

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
6.2 內存屏障

JMM內存模型中我們簡單的瞭解了下內存屏障。那麼通過前面的知識點,是不是可以猜猜volatile是不是就是通過內存屏障的方式來保證其語義的實現。
在JMM中把內存屏障指令分爲4類,通過在不同的語義下使用不同的內存屏障來進制特定類型的處理器重排序,從而來保證內存的可見性
JMM內存屏障指令

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