面試並發volatile關鍵字時,我們應該具備哪些談資?


提前發現更多精彩內容,請訪問 個人博客
提前發現更多精彩內容,請訪問 個人博客
提前發現更多精彩內容,請訪問 個人博客

寫在前面

可見性有序性,Happens-before來搞定 文章中,happens-before 的原則之一: volatile變量規則

對一個 volatile 域的寫, happens-before 於任意后續對這個 volatile 域的讀

按理說了解了這個規則,對 volatile 的使用就已經足夠了,但是面試官可是喜歡刨根問到底的,為了更透徹的了解 volatile 的內存語義與讀寫語義,為了面試多一些談資進而獲得一些加分項,同時盡早填補前序文章留下的坑,於是乎這篇文章就這樣尷尬的誕生了

happens-before 之 volatile 變量規則

下面的表格你還記得嗎?(是的,你記得😂)

能否重排序 第二個操作 第二個操作 第二個操作
第一個操作 普通讀/寫 volatile 讀 volatile 寫
普通讀/寫 - - NO
volatile 讀 NO NO NO
volatile 寫 - NO NO

上面的表格是 JMM 針對編譯器定制的 volatile 重排序的規則,那 JMM 是怎樣禁止重排序的呢?答案是內存屏障

內存屏障 (Memory Barriers / Fences)

無論你聽過這個名詞與否都沒關系,很簡單,且看

為了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序

這句話有點抽象,試着想象內存屏障是一面高牆,如果兩個變量之間有這個屏障,那么他們就不能互換位置(重排序)了,變量有讀(Load)有寫(Store),操作有前有后,JMM 就將內存屏障插入策略分為 4 種:

  1. 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障
  2. 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障
  3. 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障
  4. 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障

1 和 2 用圖形描述以及對應表格規則就是下面這個樣子了:

3 和 4 用圖形描述以及對應表格規則就是下面這個樣子了:

其實圖形也是表格內容的體現,只不過告訴大家內存屏障是如何禁止指令重排序的,所以大家只要牢記表格內容即可

一段程序的讀寫通常不會像上面兩種情況這樣簡單,這些屏障組合起來如何使用呢?其實一點都不難,我們只需要將這些指令帶入到文章開頭的表格中,然后再按照程序順序拼接指令就好了

來看一小段程序:

public class VolatileBarrierExample {

	private int a;
	private volatile int v1 = 1;
	private volatile int v2 = 2;

	void readAndWrite(){
		int i = v1; //第一個volatile讀
		int j = v2;	//第二個volatile讀
		a = i + j;	//普通寫
		v1 = i + 1;	//第一個volatile寫
		v2 = j * 2;	//第二個volatile寫
	}
}

將屏障指令帶入到程序就是這個樣子:

我們將上圖分幾個角度來看:

  1. 彩色是將屏障指令帶入到程序中生成的全部內容,也就是編譯器生成的「最穩妥」的方案
  2. 顯然有很多屏障是重復多余的,右側虛線框指向的屏障是可以被「優化」刪除掉的屏障

到這里你應該了解了 volatile 是如何通過內存屏障保證程序不被"擅自"排序的,那 volatile 是如何保證可見性的呢?

volatile 寫-讀的內存語義

回顧一下之前文章內容中的程序,假定線程 A 先執行 writer 方法,隨后線程 B 執行 reader 方法,:

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
		}
	}
}

到這里你是否還記得之前說過的 JMM,是的,你還記得😂,當線程 A 執行 writer 方法時,且看下圖:

線程 A 將本地內存更改的變量寫回到主內存中

volatile 讀的內存語義:

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

所以當線程 B 執行 reader 方法時,圖形結構就變成了這個樣子:

線程 B 本地內存變量無效,從主內存中讀取變量到本地內存中,也就得到了線程 A 更改后的結果,這就是 volatile 是如何保證可見性的

如果你看過前面的文章你就不難理解上面的兩張圖了,綜合起來說:

  1. 線程 A 寫一個volatile變量, 實質上是線程 A 向接下來將要讀這個 volatile 變量的某個線程發出了(其對共享變量所做修改的)消息
  2. 線程 B 讀一個 volatile 變量,實質上是線程 B 接收了之前某個線程發出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。
  3. 線程 A 寫一個 volatile 變量, 隨后線程 B 讀這個 volatile 變量, 這個過程實質上是線程 A 通過主內存向線程B 發送消息。

到這里,面試 volatile 時,你應該有一些談資了,同時也對 volatile 的語義有了更深層次的了解

彩蛋

之前的文章提到過這樣一句話:

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

記住文中最后兩張圖, 當我們說到 synchronized 的時候,你就會猛的理解這句話的含義了, 感興趣的可以自己先了解 synchronized 的寫-讀語義

接下來我們就聊一聊鎖相關的內容了,敬請期待...

靈魂追問

  1. 如果 volatile 寫之后直接 return,那還會生成 StoreLoad 指令嗎?
  2. synchronized 是怎樣逐步被優化的?

提高效率工具

tool.lu

https://tool.lu 是一款集成了非常多功能的在線工具,基本滿足日常開發所需


推薦閱讀


歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術干貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕松趣味學習 Java 技術棧相關知識,本着將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


免責聲明!

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



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