當一個共享變量被volatile修飾時,它會保證修改的值立即被更新到主存“, 這里的”保證“ 是如何做到的?和 JIT的具體編譯后的CPU指令相關吧?
volatile特性
內存可見性:通俗來說就是,線程A對一個volatile變量的修改,對於其它線程來說是可見的,即線程每次獲取volatile變量的值都是最新的。
volatile的使用場景
通過關鍵字sychronize可以防止多個線程進入同一段代碼,在某些特定場景中,volatile相當於一個輕量級的sychronize,因為不會引起線程的上下文切換,但是使用volatile必須滿足兩個條件:
1、對變量的寫操作不依賴當前值,如多線程下執行a++,是無法通過volatile保證結果准確性的;
2、該變量沒有包含在具有其它變量的不變式中,這句話有點拗口,看代碼比較直觀。
public class NumberRange {
private volatile int lower = 0;
private volatile int upper = 10;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
上述代碼中,上下界初始化分別為0和10,假設線程A和B在某一時刻同時執行了setLower(8)和setUpper(5),且都通過了不變式的檢查,設置了一個無效范圍(8, 5),所以在這種場景下,需要通過sychronize保證方法setLower和setUpper在每一時刻只有一個線程能夠執行。
下面是我們在項目中經常會用到volatile關鍵字的兩個場景:
1、狀態標記量
在高並發的場景中,通過一個boolean類型的變量isopen,控制代碼是否走促銷邏輯,該如何實現?
public class ServerHandler {
private volatile isopen;
public void run() {
if (isopen) {
//促銷邏輯
} else {
//正常邏輯
}
}
public void setIsopen(boolean isopen) {
this.isopen = isopen
}
}
場景細節無需過分糾結,這里只是舉個例子說明volatile的使用方法,用戶的請求線程執行run方法,如果需要開啟促銷活動,可以通過后台設置,具體實現可以發送一個請求,調用setIsopen方法並設置isopen為true,由於isopen是volatile修飾的,所以一經修改,其他線程都可以拿到isopen的最新值,用戶請求就可以執行促銷邏輯了。
2、double check
單例模式的一種實現方式,但很多人會忽略volatile關鍵字,因為沒有該關鍵字,程序也可以很好的運行,只不過代碼的穩定性總不是100%,說不定在未來的某個時刻,隱藏的bug就出來了。
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不過在眾多單例模式的實現中,我比較推薦懶加載的優雅寫法Initialization on Demand Holder(IODH)。
public class Singleton {
static class SingletonHolder {
static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
如何保證內存可見性?
在java虛擬機的內存模型中,有主內存和工作內存的概念,每個線程對應一個工作內存,並共享主內存的數據,下面看看操作普通變量和volatile變量有什么不同:
1、對於普通變量:讀操作會優先讀取工作內存的數據,如果工作內存中不存在,則從主內存中拷貝一份數據到工作內存中;寫操作只會修改工作內存的副本數據,這種情況下,其它線程就無法讀取變量的最新值。
2、對於volatile變量,讀操作時JMM會把工作內存中對應的值設為無效,要求線程從主內存中讀取數據;寫操作時JMM會把工作內存中對應的數據刷新到主內存中,這種情況下,其它線程就可以讀取變量的最新值。
volatile變量的內存可見性是基於內存屏障(Memory Barrier)實現的,什么是內存屏障?內存屏障,又稱內存柵欄,是一個CPU指令。在程序運行時,為了提高執行性能,編譯器和處理器會對指令進行重排序,JMM為了保證在不同的編譯器和CPU上有相同的結果,通過插入特定類型的內存屏障來禁止特定類型的編譯器重排序和處理器重排序,插入一條內存屏障會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序。
代碼示例如下:
class Singleton {
private volatile static Singleton instance;
private int a;
private int b;
private int b;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
a = 1; // 1
b = 2; // 2
instance = new Singleton(); // 3
c = a + b; // 4
}
}
}
return instance;
}
}
1、如果變量instance沒有volatile修飾,語句1、2、3可以隨意的進行重排序執行,即指令執行過程可能是3214或1324。
2、如果是volatile修飾的變量instance,會在語句3的前后各插入一個內存屏障。
通過觀察volatile變量和普通變量所生成的匯編代碼可以發現,操作volatile變量會多出一個lock前綴指令:
Java代碼:
instance = new Singleton();
匯編代碼:
0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
這個lock前綴指令相當於上述的內存屏障,提供了以下保證:
1、將當前CPU緩存行的數據寫回到主內存;
2、這個寫回內存的操作會導致在其它CPU里緩存了該內存地址的數據無效。
CPU為了提高處理性能,並不直接和內存進行通信,而是將內存的數據讀取到內部緩存(L1,L2)再進行操作,但操作完並不能確定何時寫回到內存,如果對volatile變量進行寫操作,當CPU執行到Lock前綴指令時,會將這個變量所在緩存行的數據寫回到內存,不過還是存在一個問題,就算內存的數據是最新的,其它CPU緩存的還是舊值,所以為了保證各個CPU的緩存一致性,每個CPU通過嗅探在總線上傳播的數據來檢查自己緩存的數據有效性,當發現自己緩存行對應的內存地址的數據被修改,就會將該緩存行設置成無效狀態,當CPU讀取該變量時,發現所在的緩存行被設置為無效,就會重新從內存中讀取數據到緩存中。

