前言:雖然這幾天寫并發的閱讀數不是很多,但是我還是想提一句。java并發基礎+設計模式真的太重要了!地基打的牢固,架構和源碼的閱讀我們才能更加得心應手,到我們有能力閱讀源碼的時候也能幫助我們提升效率。(當然對于一些大佬來說,學習不同的思維也不是壞事兒,而且也希望大佬們能指正我文章中的錯誤。誠懇!)。
今天我也還是重複的談一下Volatile關鍵字。
happens-before規則之一:
對⼀個 volatile 域的寫, happens-before 于任意後續對這個 volatile 域的讀。(合集文章由具體的解釋哈)
JMM 是怎樣禁止重排序的呢?
記憶體屏障(Memory Barriers / Fences)
為了實作 volatile 的記憶體語義,編譯器在⽣成位元組碼時,會在指令序列中插⼊記憶體屏障來禁⽌特定類型的處理器重排序
這句話有點抽象,試着想象記憶體屏障是一面高強牆,如果兩個變量之間有這個屏障,那麼他們就不能互換位置(重排序)了,變量有讀(Load)有寫(Store),操作有前有後,JMM 就将記憶體屏障插⼊政策分為4種:
- 在每個 volatile 寫操作的前⾯插⼊⼀個 StoreStore 屏障
- 在每個 volatile 寫操作的後⾯插⼊⼀個 StoreLoad 屏障
- 在每個 volatile 讀操作的後⾯插⼊⼀個 LoadLoad 屏障
- 在每個 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寫
}
}
将屏障指令帶入到程式就是這個樣子:
我們将上圖分幾個角度來看:
- 彩色是将屏障指令帶⼊到程式中⽣成的全部内容,也就是編譯器⽣成的「最穩妥」的⽅案
- 顯然有很多屏障是重複多餘的,右側虛線框指向的屏障是可以被「優化」删除掉的屏障
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方法時:
線程 A 将本地記憶體更改的變量寫回到主記憶體中。
volatile 讀的記憶體語義:
當讀⼀個 volatile 變量時, JMM 會把該線程對應的本地記憶體置為⽆效。線程接下來将從主記憶體中讀取共享變量
是以當線程 B 執⾏ reader 方法時,圖形結構就變成了這個樣⼦
線程 B 本地記憶體變量無效,從主記憶體中讀取變量到本地記憶體中,也就得到了線程 A更改後的結果,這就是 volatile 是如何保證可見性的
如果你看過前⾯的⽂章你就不難了解上⾯的兩張圖了,綜合起來說:
- 線程 A 寫⼀個volatile變量, 實質上是線程 A 向接下來将要讀這個 volatile 變量的某個線程發出了(其對共享變量所做修改的)消息
- 線程 B 讀⼀個 volatile 變量,實質上是線程 B 接收了之前某個線程發出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。
- 線程 A 寫⼀個 volatile 變量, 随後線程 B 讀這個 volatile 變量, 這個過程實質上是線程 A 通過主記憶體向線程B 發送消息。