這個Java的volatile關鍵字是用來標示一個Java變量作爲“正在被存儲在主內存的”。更加準確地說意味着,一個volatile變量的每一次讀取都是從計算機的主內存中讀取,而不是從CPU緩存中,並且對於一個volatile變量的每一次寫將會寫到主內存中,而不只是寫入到CPU緩存中。
事實上,自從Java5開始這個volatile關鍵字不只是保證變量寫到主內存,而且還從主內存中讀取。我將會在接下來的部分中解釋。
Java的volatile關鍵字的可見性的保證
這個Java的volatile關鍵字保證橫跨線程中對於變量改變的可見性。這個可能聽起來有點抽象,讓我們詳細闡述一下。
在一個多線程的應用中,線程操作在非volatile變量上,每一個線程在工作的時候可能會從主內存拷貝變量進入到CPU緩存中,因爲性能原因。如果你的計算機包含不只是一個CPU,那每一個線程可能會運行在不同的CPU中。那就意味着,每一個線程就會拷貝變量進入到不同的CPU的CPU緩存中去。如下圖所示:
使用非volatile的變量,這裏不能保證JVM什麼時間從主內存讀取數據進入到CPU緩存中,或者寫數據從CPU緩存進入主內存中。這個可能就會引起在下面部分將會解釋的幾個問題。
想象一個場景,兩個或者更多的線程訪問一個包含聲明瞭一個counter變量的一個共享對象,像下面這樣:
public class SharedObject {
public int counter = 0;
}
也想象一下,只有線程1增加counter變量,但是線程1和線程2偶爾會讀取這個counter變量。
如果這個counter變量沒有聲明爲volatile,那就不能保證這個counter變量什麼時間從CPU緩存中寫回到主內存中。這個就意味着,這個在CPU緩存中的counter變量跟在主內存中的值是不同的。如下圖所示:
線程不能看到這個變量的最新的值的這個問題是因爲它還沒有被其他的線程寫回到主內存中,別稱之爲”可見性”問題。一個線程的更新對於其他的線程是不可見的。
通過聲明這個counter變量爲volatile,對於counter這個變量的所有寫入將會立刻寫回到主內存中。同時,對於counter變量的所有讀取將會直接從主內存中讀取。這裏有一個counter變量怎麼樣聲明爲volatile:
public class SharedObject {
public volatile int counter = 0;
}
聲明一個變量爲volatile,因此可以保證針對這個變量的寫對於其他線程的可見性。
這個Java的volatile關鍵字保證了前後順序
自從Java5以來,這個volatile關鍵字不只是保證了變量從主內存的讀和寫。實際上,volatile關鍵字還保證了這個:
- 如果線程A寫向一個volatile變量,以及線程B隨後讀取這個變量,然後在寫這個volatile變量之前,所有的變量對於線程A是可見的,在它已經讀取這個volatile變量之後也會對線程B可見的。
- volatile變量的讀和寫的指令不會被JVM重排序(只要JVM檢測到只要來自於重排序的程序活動沒有改變,JVM可能因爲性能原因重排序指令)。指令可以在前和後重排序,但是volatile關鍵字的讀或者寫不會跟這些指令混合。無論跟隨一個volatile變量的讀或者寫的指令是什麼,都會保證讀或者寫的前後順序。
Thread A:
sharedObject.nonVolatile = 123;
sharedObject.counter = sharedObject.counter + 1;
Thread B:
int counter = sharedObject.counter;
int nonVolatile = sharedObject.nonVolatile;
因爲在寫這個volatile的counter之前,線程A寫了非volatile得nonVolatile變量,然後當線程A寫這個counter(volatile變量)的時候,非volatile得變量也被寫回到了主內存中。
public class Exchanger {
private Object object = null;
private volatile hasNewObject = false;
public void put(Object newObject) {
while(hasNewObject) {
//wait - do not overwrite existing new object
}
object = newObject;
hasNewObject = true; //volatile write
}
public Object take(){
while(!hasNewObject){ //volatile read
//wait - don't take old object (or null)
}
Object obj = object;
hasNewObject = false; //volatile write
return obj;
}
}
線程A可能會通過不斷的調用put方法設置對象。線程B可能會通過不斷的調用take方法獲取這個對象。這個類可以工作的很好通過使用一個volatile變量(沒有使用synchronized鎖),只要只是線程A調用put方法,線程B調用take方法。
while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
注意這個volatile變量的寫是在新的對象被真實賦值之前執行的。對於JVM這個可能看起來是完全正確的。這兩個寫的執行的值不會互相依賴。
sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;
sharedObject.volatile = true; //a volatile variable
int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;
JVM可能會重排序前面的三個指令,只要他們中的所有在volatile寫執行發生前(他們必須在volatile寫指令發生前執行)。
翻譯地址:http://tutorials.jenkov.com/java-concurrency/volatile.html