天天看點

Java并發!Volatile關鍵字應該具備哪些面試談資?

前言:雖然這幾天寫并發的閱讀數不是很多,但是我還是想提一句。java并發基礎+設計模式真的太重要了!地基打的牢固,架構和源碼的閱讀我們才能更加得心應手,到我們有能力閱讀源碼的時候也能幫助我們提升效率。(當然對于一些大佬來說,學習不同的思維也不是壞事兒,而且也希望大佬們能指正我文章中的錯誤。誠懇!)。

今天我也還是重複的談一下Volatile關鍵字。

happens-before規則之一:

對⼀個 volatile 域的寫, happens-before 于任意後續對這個 volatile 域的讀。(合集文章由具體的解釋哈)

JMM 是怎樣禁止重排序的呢?

記憶體屏障(Memory Barriers / Fences)

為了實作 volatile 的記憶體語義,編譯器在⽣成位元組碼時,會在指令序列中插⼊記憶體屏障來禁⽌特定類型的處理器重排序

這句話有點抽象,試着想象記憶體屏障是一面高強牆,如果兩個變量之間有這個屏障,那麼他們就不能互換位置(重排序)了,變量有讀(Load)有寫(Store),操作有前有後,JMM 就将記憶體屏障插⼊政策分為4種:

  1. 在每個 volatile 寫操作的前⾯插⼊⼀個 StoreStore 屏障
  2. 在每個 volatile 寫操作的後⾯插⼊⼀個 StoreLoad 屏障
  3. 在每個 volatile 讀操作的後⾯插⼊⼀個 LoadLoad 屏障
  4. 在每個 volatile 讀操作的後⾯插⼊⼀個 LoadStore 屏障

1 和 2 ⽤圖形描述以及對應表格規則就是下⾯這個樣⼦了:

Java并發!Volatile關鍵字應該具備哪些面試談資?

3 和 4 ⽤圖形描述以及對應表格規則就是下⾯這個樣⼦了:

Java并發!Volatile關鍵字應該具備哪些面試談資?

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

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

将屏障指令帶入到程式就是這個樣子:

Java并發!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
 }
 }
}           

當線程 A 執⾏ writer方法時:

Java并發!Volatile關鍵字應該具備哪些面試談資?

線程 A 将本地記憶體更改的變量寫回到主記憶體中。

volatile 讀的記憶體語義:

當讀⼀個 volatile 變量時, JMM 會把該線程對應的本地記憶體置為⽆效。線程接下來将從主記憶體中讀取共享變量

是以當線程 B 執⾏ reader 方法時,圖形結構就變成了這個樣⼦

Java并發!Volatile關鍵字應該具備哪些面試談資?

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

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

  1. 線程 A 寫⼀個volatile變量, 實質上是線程 A 向接下來将要讀這個 volatile 變量的某個線程發出了(其對共享變量所做修改的)消息
  2. 線程 B 讀⼀個 volatile 變量,實質上是線程 B 接收了之前某個線程發出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。
  3. 線程 A 寫⼀個 volatile 變量, 随後線程 B 讀這個 volatile 變量, 這個過程實質上是線程 A 通過主記憶體向線程B 發送消息。