首頁  >  文章  >  Java  >  Java的Volatile關鍵字詳解

Java的Volatile關鍵字詳解

黄舟
黄舟原創
2017-02-28 10:48:591948瀏覽

這個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變數的讀取或寫入的指令是什麼,都會保證讀或寫的前後順序。

這些表述需要更深的解釋。

當一個執行緒寫一個volatile變數的時候,然後不只是這個volatile變數他自己被寫回到主記憶體。在寫這個volatile變數之前的被這個線程改變的所有其他的變數也會寫回主記憶體。當一個執行緒讀取一個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得變數也被寫回了主記憶體。

因為線程B開始讀取counter這個volatile變量,然後這個counter變數和nonVolatile變數都會被線程B從主記憶體讀取到CPU快取中。這時候線程B也會看到被線程A寫的這個nonVolatile變數。

開發者可能會使用這個擴充功能的可見性保證來優化執行緒之間變數的可見性。取代聲明每一個變數為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方法。

然而,JVM可能重排序Java指令去优化性能,如果JVM可以做这个没有改变这个重排序的指令。如果JVM改变了put方法和take方法内部的读和写的顺序将会发生什么呢?如果put方法真的像下面这样执行:

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


注意这个volatile变量的写是在新的对象被真实赋值之前执行的。对于JVM这个可能看起来是完全正确的。这两个写的执行的值不会互相依赖。

然而,重排序这个执行的执行将会危害object变量的可见性。首先,线程B可能在线程A确定的写一个新的值给object变量之前看到hasNewObject这个值设为true了。第二,现在甚至不能保证对于object的新的值是否会写回到主内存中。

为了阻止上面所说的那种场景发生,这个volatile关键字提供了一个“发生前保证”。保证volatile变量的读和写指令执行前不会发生重排序。指令前和后是可以重排序的,但是这个volatile关键字的读和写指令是不能发生重排序的。

看这个例子:

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写指令发生前执行)。

类似的,JVM可能重排序最后3个指令,只要volatile写指令在他们之前发生。最后三个指令在volatile写指令之前都不会被重排序。

那个基本上就是Java的volatile保证先行发生的含义了。

volatile关键字不总是足够的

甚至如果volatile关键字保证了volatile变量的所有读取都是从主内存中读取,以及所有的写也是直接写入到主内存中,但是这里仍然有些场景声明volatile是不够的。

在更早解释的场景中,只有线程1写这个共享的counter变量,声明这个counter变量为volatile是足够确保线程2总是看到最新写的值。

事实上,如果在写这个变量的新的值不依赖它之前的值得情况下,甚至多个线程写这个共享的volatile变量,仍然有正确的值存储在主内存中。换句话说,如果一个线程写一个值到这个共享的volatile变量值中首先不需要读取它的值去计算它的下一个值。

如果一个线程需要首先去读取这个volatile变量的值,并且建立在这个值的基础上去生成一个新的值,那么这个volatile变量对于保证正确的可见性就不够了。在读这个volatile变量和写新的值之间的短时间间隔,出现了一个竞态条件,在这里多个线程可能会读取到volatile变量的相同的值生成一个新的值,并且当写回到主内存中的时候,会互相覆盖彼此的值。

多个线程增加相同的值得这个场景,正好一个volatile变量不够的。下面的部分将会详细解析这个场景。

想象下,如果线程1读取值为0的共享变量counter进入到CPU缓存中,增加1并且没有把改变的值写回到主内存中。线程2读取相同的counter变量从主内存中进入到CPU缓存中,这个值仍然为0。线程2也是加1,并且也没有写入到主内存中。这个场景如下图所示:


线程1和线程2现在是不同步的。这个共享变量的真实值应该是2,但是每一个线程在他们的CPU缓存中都为1,并且在主内存中的值仍然是0.它是混乱的。甚至如果线程最后写他们的值进入主内存中,这个值是错误的。

什么时候volatile是足够的

正如我前面提到的,如果两个线程都在读和写一个共享的变量,然后使用volatile关键字是不够的。你需要使用一个synchronized在这种场景去保证这个变量的读和写是原子性的。读或者写一个volatile变量不会堵塞正在读或者写的线程。因为这个发生,你必须使用synchronized关键字在临界区域周围。

作为一个synchronized锁可选择的,你也可以使用在java.util.concurrent包中的许多原子数据类型中的一个。例如,这个AtomicLong或者AtomicReference或者是其他中的一个。

假如只有一个线程读和写这个volatile变量的值,其他的线程只是读取这个变量,然后读的这个线程就会保证看到最新的值了。不使用这个volatile变量,这个就不能保证。

volatile關鍵字的效能考慮

volatile變數的讀取和寫入引起了這個變數將會讀或寫到主記憶體。從主記憶體讀取或寫到主記憶體比存取CPU快取有更大的消耗。存取volatile變數也會阻止指令重排序,這也是一個標準的效能增加技術。因此,你應該只有當你真正的需要變數的強烈可見性的時候應該使用volatile變數。

 以上就是Java的Volatile關鍵字詳解的內容,更多相關內容請關注PHP中文網(www.php.cn)!


#
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn