Java Volatile Keyword - 譯文

徐家三少發表於2017-03-16

Java Volatile Keyword

  併發是程式界的量子物理,然而volatile又是量子物理中薛定諤的貓。本篇文章試圖系統的梳理一下java中的Volatile關鍵字。這篇譯文可能幫助你更好的理解volatile關鍵字。
   使用volatile關鍵字是解決同步問題的一種有效手段。 java volatile關鍵字預示著這個變數始終是“儲存進入了主存”。更精確的表述就是每一次讀一個volatile變數,都會從主存讀取,而不是CPU的快取。同樣的道理,每次寫一個volatile變數,都是寫回主存,而不僅僅是CPU的快取。

Java Volatile Keyword - 譯文

  事實上,JAVA5的volatile關鍵字不只是保證了每次從主存讀寫資料。下面將著重介紹volatile關鍵字的特性。

  Java 保證volatile關鍵字保證變數的改變對各個執行緒是可見的。這看起來有點抽象,不過將緊接著說明這一點。
我們知道,每一個執行緒都有自己的執行緒棧。多執行緒在操作非volatile變數的時候,都會從主存拷貝變數值到自己的棧記憶體中間,然後再操作變數。在多個執行緒的情況下,如果一個執行緒修改了變數值還未回寫到主記憶體,另一個執行緒讀取的就是一箇舊的值,這樣會出現問題,因為讀到的變數不是最新的。實際上,在多核CPU中間,由於每個CPU都有自己的快取,同樣會存在主存與CPU快取之間資料不一致的情況。因此,在C語言中,也有volatile關鍵字。(譯者注:實際上,如果在CPU的層面滿足volatile特性,那麼執行緒棧就一定滿足。因為從volatile語義來講,jvm執行緒每次只從主存讀寫volatile變數,而主存的volatile變數又在CPU層面滿足volatile語義)
想象一種這樣的情況,有兩個或者更多的執行緒訪問一個共享物件,這個共享物件包括了一個counter變數:
public class SharedObject {

public int counter = 0;複製程式碼

}
  再想象一下,只有執行緒1對counter變數加一,但是執行緒一和執行緒2卻是同時讀到這個變數。

  如果這個contouer變數沒有被宣告為volatile。
就不能保證counter變數從cpu快取回寫到主存。這就意味著counter變數在cpu快取中的值與主存中值不一致。

  這就是所謂的執行緒不能看到變數最新值的問題。因為另外一個執行緒並沒有及時將變數寫回到主存。這樣一個執行緒的人更新對其他執行緒是不可見的。

  通過宣告counter變數是一個volatile變數,這樣所有counter變數的更改就會被立即寫入主存。同樣,對counter變數的讀也從主存裡面讀。下面是如何宣告一個volatile變數:

public class SharedObject {

    public volatile int counter = 0;

}複製程式碼

  通過宣告volatile變數就保證了對其他執行緒寫的可見性。

java volatile的happen-before保證

  java5中的volatile關鍵字不只是保證從主存中讀寫資料,實際上,volatile還保證如下的情況:

  • 如果執行緒a寫一個volatile變數,隨後執行緒b讀取這個變數,然後所有的變數線上程a寫之前可見,所有的變數也在b讀之後對執行緒b可見了。(譯者注:volatile有兩個語義:可見性與讀寫原子性。a在寫變數的過程中,b是無法讀取的。因為CPU會鎖定這塊記憶體區域的快取並回寫到記憶體。此時B才可以讀取,如果A在寫的過程中B可以讀取,那麼執行緒B讀取的是髒資料。i++之所以無法用volatile保證原子性。是因為volatile僅僅保證讀取加鎖,賦值加鎖,而對於中間的加1操作是不會加鎖的。執行緒B如果在這個期間讀取值,那肯定會是髒資料。)

  讀和寫volatile變數的指令無法被JVM重排序(JVM為了提高效能可以重排序一些指令,只要程式的行為與排序前一樣)但是volatile變數卻無法重排序,也就是volatile變數的讀和寫無法被打亂在其他變數中間。不管是什麼指令,總是在volatile變數讀寫之後發生。

  下面將會詳細的解釋這一點。

  當一個執行緒寫一個volatile變數,然後不僅僅是volatile變數本身自身寫入到主存。所有其他的在寫volatile變數之前也會被刷入主存。當一個執行緒讀volatile變數的時候,它也會從主存讀取其他變數。(譯者注:注意是所有的變數。每次在寫入volatile變數的時候,執行緒棧裡面的所有的共享變數都將刷回主存,而不僅僅是在volatile變數宣告之前的變數)

  看下面這個例子,sharedObject.counter是一個volatile變數:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;複製程式碼

  當執行緒A寫在寫入volatile變數sharedObject.counter之前寫入一個非volatile變數,然後再寫入volatile變數,這個時候非volatile變數sharedObject.nonVolatile 也會被寫入主存。

  當執行緒B開始讀一個volatile變數sharedObject.counter,然後所有的sharedObject.nonVolatile以及

  sharedObject.counter都會從主存讀取。這個時候sharedObject.nonVolatile值與執行緒A中的值是一樣的。

  開發者可以使用這種擴充套件的可視性來優化執行緒之間的可視性:不是對每個變數都宣告為volatile變數,而是隻需要宣告其中一部分變數為volatile。下面是Exchanger類,就利用了上述的原則:

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方法。這個Exchanger能夠在合理使用volatile關鍵字的情況下工作的很好。只要執行緒A只呼叫put方法,執行緒b只呼叫take方法。

  然而,JVM是可以對指令進行優化的。如果JVM對指令優化,打亂了順序,會出現什麼樣的效果呢?下面這段程式碼可能是執行的順序之一:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;複製程式碼

  注意到volatile變數hasNewObject現在在object被設定之前執行了。這個對於JVM來說看起來好像是合法的,因為這兩個值的寫入指令相互是沒有依賴的,JVM可以對它們重排序。

  然而,重排指令有可能影響到object變數的可見性。首先,執行緒B看見線上程A還沒有對object賦值之前就看見了hasNewObject是一個true變數,這樣操作執行緒B讀取了一個空值。其次,這甚至不能保證object變數會被及時的寫入到主存。(當然,下一次執行緒A更改volatile變數的時候就會被刷進主存)

  為了阻止上面的任何一種情況發生,volatile保證了“happens before ”特性。happens-before特性保證volatile變數的讀寫不能被重排序。也就是對volatile變數的讀寫不能插入到其他的任何指令中。

  看下面這個例子:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //一個 volatile 變數

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;複製程式碼

  JVM 可能重排序前三個指令。只要他們全部在volatile寫入指令前發生(他們必須在volatile寫入前全部執行)

  類似的,JVM可能重排序最後三個指令。只要volatile變數寫操作在它們前發生。最後這三個指令都不能被排在volatile變數寫指令前面。

  這就是最基本的javavolatile變數的happens before原則。

volatile通常是不夠的。

  即使是volatile關鍵字保證了讀寫都是從主存讀取,然而仍然有寫情況不能簡單的使用variable變數來解決。在早先講到的例子中,當執行緒1寫入一個變數counter這個volatile之後,就能保證執行緒2讀到這個最新的值。

  事實上,如果執行緒在寫volatile變數並不依賴於這個volatile之前的值,那麼在寫的過程中,主存中仍然是當前的值。

  然後一個執行緒開始讀這個volatile變數。那麼這個執行緒讀到的值就是舊的值,可見性就是不正確的。這就會造成讀變數和寫變數之間的競爭。volatile關鍵字只是保證了下一次讀取的是最新的變數,但是在另外一個變數寫入的過程中,讀到的值仍然是舊的。(譯者注:如果是多個CPU先寫後讀,在寫的過程中實際上會發出訊號,告知其快取已經失效,所以並不會存在這種情況;至於先讀後寫,讀取一箇舊的值的時候要在程式碼裡保證並不會引發任何錯誤。)。

相關文章