volatile
- volatile 只能保證對單次讀/寫的原子性。i++ 這種符合操作操作不能保證原子性。
- 禁止指令重排
- 可見性
volatile讀的內存語義
當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量(注意不僅僅是一個volatile變量,是所有共享變量)
volatile寫的內存語義
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存(注意不僅僅是一個volatile變量,是所有共享變量)
可見性
可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。
如果一個字段被聲明為volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。
如何保證可見性
我們使用X86處理器下通過工具獲取JIT編譯器生成的匯編指令來查看對volatile進行寫操作會發生什么
instance = new Singleton();
轉成匯編代碼如下
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
我們發現了lock前綴的指令,Lock前綴的指令在多核處理器下會發生兩件事情:
- JVM中將當前線程工作內存中的數據寫回主內存中(底層中是將當前處理器緩存行的數據寫回到系統內存)
- 使得其他線程工作內存中的該地址的數據無效(底層中是這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效)
為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內存。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。
重排序
重排序是指編譯器和處理器為了優化程序性能在不影響程序執行結果的前提下而對指令序列進行重新排序的一種手段。
數據依賴性
如果兩個操作訪問同一個變量,且至少一個操作是寫操作,此時這兩個操作之間就存在數據依賴性。
名稱 | 代碼示例 | 說明 |
---|---|---|
寫后讀 | a=1;b=a; | 寫一個變量后,再讀這個變量 |
寫后寫 | a=1;b=1; | 寫一個變量后,再寫這個變量 |
讀后寫 | a=b;b=1; | 讀一個變量后,再寫這個變量 |
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果就會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器再重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的順序。
這里說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不會被考慮,也就是說多線程下重排序可能會影響執行的結果。
重排序對多線程的影響
現在有AB兩個線程,A線程調用writer()
方法,B線程調用reader()
方法。因為A線程中的操作1與操作2不具備數據依賴性,所以會發生指令重排,同樣操作3和操作4也不具備數據依賴性。當線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入?不一定!
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
}
所以說多線程下重排序可能會影響執行的結果
內存屏障
內存屏障是一個 CPU 指令,不同的硬件實現內存屏障的手段不一樣,java通過屏蔽這些差異,統一由jvm來生成內存屏障的指令。
java中內存屏障的兩個作用
-
阻止屏障兩側的代碼指令重排序
-
強制將工作內存中的數據寫回主內存,讓工作內存中的數據失效。
java中的內存屏障分類
- LoadLoad屏障:LoadLoad屏障前面的讀操作不能與后面的所有讀操作重排序
- StoreStore屏障:StoreStore屏障前面的寫操作不能與后面的所有寫操作重排序
- LoadStore屏障:LoadStore屏障前面的讀操作不能與后面的所有寫操作重排序
- StoreLoad屏障:StoreLoad屏障前面的寫操作不能與后面的所有讀操作重排序
java中內存屏障插入策略
- 在每個volatile寫操作的前面插入一個StoreStore屏障
- 在每個volatile寫操作的后面插入一個StoreLoad屏障
- 在每個volatile讀操作的前面插入一個LoadLoad屏障
- 在每個volatile讀操作的后面插入一個LoadStore屏障
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
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寫
}
… // 其他方法
}

可以看出策略及其保守,但是在連續的加屏障操作中,jvm會自行進行優化,除去不必要的屏障。這就叫:首先保證正確性,然后再去追尋執行效率。
所有寫操作的后面插入StoreLoad屏障還有一個重要作用,因為寫操作之后方法可能會return,此時編譯器無法確定后面時候會有bolatile讀或寫,所以為了安全起見,編譯器通常會在這里插入一個StoreLoad屏障。
部分參考《並發編程的藝術》