Java Volatile Keyword - 譯文

Java Volatile Keyword

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


  事實上,JAVA5的volatile關鍵字不僅是保證了每次從主存讀寫數據。下面將着重介紹volatile關鍵字的特性。

  Java 保證volatile關鍵字保證變量的改變對各個線程是可見的。這看起來有點抽象,不過將緊接着說明這一點。
咱們知道,每個線程都有本身的線程棧。多線程在操做非volatile變量的時候,都會從主存拷貝變量值到本身的棧內存中間,而後再操做變量。在多個線程的狀況下,若是一個線程修改了變量值還未回寫到主內存,另外一個線程讀取的就是一箇舊的值,這樣會出現問題,由於讀到的變量不是最新的。實際上,在多核CPU中間,因爲每一個CPU都有本身的緩存,一樣會存在主存與CPU緩存之間數據不一致的狀況。所以,在C語言中,也有volatile關鍵字。(譯者注:實際上,若是在CPU的層面知足volatile特性,那麼線程棧就必定知足。由於從volatile語義來說,jvm線程每次只從主存讀寫volatile變量,而主存的volatile變量又在CPU層面知足volatile語義)
想象一種這樣的狀況,有兩個或者更多的線程訪問一個共享對象,這個共享對象包括了一個counter變量:
public class SharedObject {java

public int counter = 0;複製代碼

}
  再想象一下,只有線程1對counter變量加一,可是線程一和線程2倒是同時讀到這個變量。緩存

  若是這個contouer變量沒有被聲明爲volatile。
就不能保證counter變量從cpu緩存回寫到主存。這就意味着counter變量在cpu緩存中的值與主存中值不一致。多線程

  這就是所謂的線程不能看到變量最新值的問題。由於另一個線程並無及時將變量寫回到主存。這樣一個線程的人更新對其餘線程是不可見的。併發

  經過聲明counter變量是一個volatile變量,這樣全部counter變量的更改就會被當即寫入主存。一樣,對counter變量的讀也從主存裏面讀。下面是如何聲明一個volatile變量:app

public class SharedObject {

    public volatile int counter = 0;

}複製代碼

  經過聲明volatile變量就保證了對其餘線程寫的可見性。jvm

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變量讀寫以後發生。優化

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

  當一個線程寫一個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先寫後讀,在寫的過程當中實際上會發出信號,告知其緩存已經失效,因此並不會存在這種狀況;至於先讀後寫,讀取一箇舊的值的時候要在代碼裏保證並不會引起任何錯誤。)。

相關文章
相關標籤/搜索