簡單聊聊volatile關鍵字原理
volatile 關鍵字
一、說說你對 volatile 關鍵字的理解
被 volatile 修飾的共享變數,就具有了以下兩點特性:
-
保證了不同執行緒對該變數操作的記憶體可見性;
-
禁止指令重排序
二、記憶體可見性 和 禁止重排序分別怎麼實現的?
當一個變數被 volatile 修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中重新讀取新值。
禁止重排序,內部提供了記憶體屏障,寫的指令不能往後排,讀的指令不能往前排
1.可見性(Visibility): 說到可見性,Java就是利用volatile來提供可見性的。 當一個變數被volatile修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中讀取新值。而普通變數則不能保證這一點。 其實通過synchronized和Lock也能夠保證可見性,執行緒在釋放鎖之前,會把共享變數值都刷回主存,但是synchronized和Lock的開銷都更大。
2. 有序性(Ordering) JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程式的執行結果不能改變。加上volatile關鍵字,禁止重排序,可以確保程式的有序性,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裡的程式碼都是一次性執行完畢的。
三、volatile 關鍵字如何滿足併發程式設計的三大特性的?
當寫一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體,當讀一個volatile變數時,JMM 會把該執行緒對應的本地記憶體置為無效,執行緒接下來將從主記憶體中讀取共享變數。
四、volatile 的兩點記憶體語義能保證可見性和有序性,但是能保證原子性嗎?
不能保證原子性
比如下面的例子:
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的執行緒都執行完 Thread.yield(); System.out.println(test.inc); }
按道理來說結果是10000,但是執行下很可能是個小於10000的值。有人可能會說volatile不是保證了可見性啊,一個執行緒對inc的修改,另外一個執行緒應該立刻看到啊!可是這裡的操作inc++是個複合操作啊,包括讀取inc的值,對其自增,然後再寫回主存。 假設執行緒A,讀取了inc的值為10,這時候被阻塞了,因為沒有對變數進行修改,觸發不了volatile規則。 執行緒B此時也讀讀inc的值,主存裡inc的值依舊為10,做自增,然後立刻就被寫回主存了,為11。 此時又輪到執行緒A執行,由於工作記憶體裡儲存的是10,所以繼續做自增,再寫回主存,11又被寫了一遍。所以雖然兩個執行緒執行了兩次 increase(),結果卻只加了一次。 有人說,volatile 不是會使快取行無效的嗎?但是這裡執行緒A讀取到執行緒B也進行操作之前,並沒有修改inc值,所以執行緒B讀取的時候,還是讀的10。 又有人說,執行緒B將11寫回主存,不會把執行緒A的快取行設為無效嗎?但是執行緒A的讀取操作已經做過了啊,只有在做讀取操作時,發現自己快取行無效,才會去讀主存的值,所以這裡執行緒A只能繼續做自增了。 綜上所述,在這種複合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設定flag值的例子裡,由於對flag的讀/寫操作都是單步的,所以還是能保證原子性的。 要想保證原子性,只能藉助於synchronized,Lock以及併發包下的atomic的原子操作類了,即對基本資料型別的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數),減法操作(減一個數)進行了封裝,保證這些操作是原子性操作。
五、知道 volatile 底層的實現機制?
-
重排序時不能把後面的指令重排序到記憶體屏障之前的位置
-
使得本CPU的Cache寫入記憶體
-
寫入動作也會引起別的CPU或者別的核心無效化其Cache,相當於讓新寫入的值對別的執行緒可見。
六、 你在哪裡會使用到volatile,舉兩個例子呢?
1.狀態量標記,就如上面對flag的標記,我重新提一下:
int a = 0;
volatile bool flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a;//4
}
}
這種對變數的讀寫操作,標記為volatile可以保證修改對執行緒立刻可見。比synchronized,Lock有一定的效率提升。
2.單例模式的實現,典型的雙重檢查鎖定(DCL)
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
這是一種懶漢的單例模式,使用時才建立物件,而且為了避免初始化操作的指令重排序,給instance加上了volatile。