1. 程式人生 > >13、Java並發性和多線程-Java Volatile關鍵字

13、Java並發性和多線程-Java Volatile關鍵字

也不會 深入 spa 程序 dex bsp 谷歌 .cn new

以下內容轉自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻譯):

Java volatile關鍵字用於將Java變量標記為“存儲在主存儲器”中。更準確地說,這意味著,每個讀取volatile變量將從計算機的主存儲器中讀取,而不是從CPU緩存中讀取,並且每個寫入volatile變量的寫入將被寫入主存儲器,而不僅僅是寫入CPU緩存。

實際上,由於Java 5的volatile關鍵字保證不僅僅是volatile變量被寫入和從主內存讀取。我將在以下各節中解釋一下。

Java volatile可見性保證

Java volatile關鍵字可確保跨線程對變量的更改的可見性。這可能聽起來有點抽象,所以讓我詳細說明一下。

出於性能原因,線程在非volatile變量上運行的多線程應用程序中,每個線程可能會將變量從主存儲器復制到CPU高速緩存中。如果你的計算機包含多個CPU,則每個線程可能在不同的CPU上運行。這意味著每個線程都可以將變量復制到不同CPU的CPU緩存中。這在這裏說明了:

技術分享

對於非volatile變量,不能保證Java虛擬機(JVM)將數據從主存儲器讀取到CPU高速緩存中,或者將數據從CPU緩存寫入主存儲器。這可能會導致幾個問題,我將在以下部分中解釋。

想象一下,兩個或多個線程可以訪問共享對象的情況,該對象包含一個如下所示的計數器變量:

public class SharedObject {

    public int counter = 0;

}

想象一下,只有線程1增加counter變量,但線程1和線程2都可能對counter不時讀取變量。

如果counter未聲明變量,volatile則不能保證將counter變量的值從CPU緩存寫回主存儲器。這意味著counter在CPU緩存中的變量值可能與主內存不一樣。這種情況在這裏說明:

技術分享

沒有看到變量的最新值,因為還沒有被另一個線程寫回到主內存的線程的問題被稱為“可見性”問題。一個線程的更新對其他線程是不可見的。

通過聲明counter

變量,對變量的volatile所有寫入counter將立即寫回主內存。此外,counter變量的所有讀取將直接從主存儲器讀取。下面是如何volatile在聲明counter 變量的樣子:

public class SharedObject {

    public volatile int counter = 0;

}

因此, 聲明一個volatile變量可以保證該變量的其他寫入線程的可見性。

Java volatile事件保證

由於Java 5的volatile關鍵字不僅僅保證了對變量的主內存的讀取和寫入。實際上,volatile關鍵字保證:

  • 如果線程A寫入volatile變量和線程B隨後讀取相同的volatile變量,然後看到線程A的所有變量之前寫volatile變量,也將是可見的線程B後,它已經讀volatile變量。

  • volatile變量的讀寫指令不能被JVM重新排序(只要JVM從重新排序中沒有檢測到程序行為的變化,JVM可能會因為性能原因重新排序指令)。之前和之後的指令可以重新排序,但是這些指令不能混合寫入或寫入。無論讀取還是寫入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;

由於線程A在寫入volatile變量sharedObject.counter之前寫入非volatile變量sharedObject.nonVolatile,所以當線程A寫入sharedObject.counter(volatile變量)時,sharedObject.nonVolatile和sharedObject.counter都將寫入主內存。

由於線程B從讀取volatile的sharedObject.counter開始,所以sharedObject.counter和sharedObject.nonVolatile都從主內存讀取到線程B使用的CPU高速緩存。當線程B讀取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()來不時地獲取對象。只要線程A調用put()並且只有線程B調用take(),這個Exchanger可以使用volatile變量(不使用同步塊)來正常工作。

但是,如果JVM可以在不改變重新排序的指令的語義的情況下,JVM可以重新排序Java指令來優化性能。如果JVM切換的讀取和順序寫入裏面會發生什麽,put()take()?如果put()真的執行如下:

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

註意,在實際設置新對象之前,對volatile變量hasNewObject的寫入將被執行。對於JVM,這可能看起來完全有效。兩個寫入指令的值不依賴於彼此。

但是,重新排序指令執行會損害對象變量的可見性。首先,線程B可能會在線程A實際上為對象變量寫入一個新值之前看到hasNewObject設置為true。第二,現在甚至不能保證寫入對象的新值將被刷新回主內存(以下是線程A在某處寫入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可能會重新排序前3個指令。

類似地,只要在所有這些指令之前發生易失性寫入指令,JVM可以重新排序最後3條指令。在易失性寫入指令之前,最後3條指令都不能重新排序。

這基本上是Java保護之前發生的volatile的意思。

volatile並不總是足夠

即使volatile關鍵字保證volatile變量的所有讀取都直接從主存儲器讀取,並且對volatile變量的所有寫入都直接寫入主存儲器,仍然存在聲明volatile變量還不夠的情況。

在前面所述的情況下,只有線程1寫入共享counter變量,聲明counter變量volatile就足以確保線程2總是看到最新的寫入值。

事實上,volatile如果寫入變量的新值不依賴於其先前的值,多線程甚至可能寫入一個共享變量,並且仍然具有存儲在主存儲器中的正確值。換句話說,如果一個向共享volatile變量寫值的線程首先不需要讀取它的值來找出它的下一個值。

一旦線程需要首先讀取volatile變量的值,並且基於該值為共享volatile變量生成新值,則變量volatile不再足以保證正確的可見性。在讀取volatile變量和寫入新值之間的短時間間隙創建了一個競爭條件 ,其中多個線程可能讀取相同的volatile變量值,為變量生成一個新值,並將該值寫回到主內存-覆蓋彼此的值。

多線程增加相同計數器的情況正是這種情況,其中volatile變量不夠。以下部分將更詳細地解釋這一情況。

想象一下,如果線程1將counter值為0的共享變量讀入其CPU緩存,將其遞增到1,而不是將更改的值寫入主存儲器。線程2然後可以從counter變量的值仍然為0的主存儲器讀取相同的變量到自己的CPU緩存中。線程2也可以將計數器遞增到1,也不會將其寫回主存儲器。這種情況如下圖所示:

技術分享

線程1和線程2現在實際上不同步。共享counter變量的實際值應為2,但每個線程的CPU緩存中的變量的值為1,主內存中的值仍為0。這是一個混亂!即使線程最終將共享counter變量的值寫回到主內存中,該值也將是錯誤的。

什麽時候使用呢?

如前所述,如果兩個線程都是共享變量的讀取和寫入,則使用volatile關鍵字是不夠的。 在這種情況下,你需要使用synchronized來保證變量的讀寫是原子的。讀取或寫入volatile變量不阻止線程讀取或寫入。為了實現這一點,你必須在關鍵部分周圍使用synchronized關鍵字。

作為synchronized塊的替代,你還可以使用java.util.concurrent包中發現的許多原子數據類型之一。例如,AtomicLongAtomicReference其他人之一。

如果只有一個線程讀寫volatile變量的值,並且其他線程只讀取變量,則讀取線程將被保證看到寫入volatile變量的最新值。在不變量變動的情況下,這不能保證。

volatile關鍵字保證在32位和64變量上工作。

性能考慮波動

讀寫volatile變量會導致變量被讀取或寫入主存儲器。讀取和寫入主內存比訪問CPU緩存更昂貴。訪問volatile變量還可以防止指令重新排序,這是正常的性能增強技術。因此,當你真正需要強制實現變量的可見性時,你應該只使用volatile變量。

13、Java並發性和多線程-Java Volatile關鍵字