1. 程式人生 > >java學習之volatile

java學習之volatile

解釋 params eval 變量 ble -a 優化 sync mic

轉載:http://lucumt.info/posts/java-concurrency/java-volatile-keyword/

Java關鍵字volatile用於將一個Java變量標記為 在主內中存儲 ,更準確的解釋為:每次讀取一個volatile變量時將從電腦的主內存中讀取而不是從CPU緩存中讀取,每次對一個volatile變量進行寫操作時,將會寫入到主內存中而不是寫入到CPU緩存中。

事實上,從Java5之後,volatile關鍵字不僅僅可以用來確保volatile變量是寫入到主內存和從主內存讀取數據,我會在下面的章節進行詳細的介紹:

Volatile變量可見性保證

Java volatile

關鍵字確保了volatile變量的修改在多線程中是可見的。這聽起來有些抽象,接下來我將詳細說明。

在一個對非volatile變量進行操作的多線程應用,由於性能的關系,當對這些變量進行讀寫時,每個線程都可能從主線程中拷貝變量到CPU緩存中。如果你的電腦不止一個CPU,每個線程可能會在不同的CPU上運行。這意味著,每個線程都可能將變量拷貝到不同的CPU的CPU緩存中,如下圖所示: 技術分享
對於volatile變量而言,Java虛擬機(JVM)不能確保什麽時候將數據從主內存讀取到CPU緩存以及什麽時候將CPU緩存的數據寫入到主內存中。而這可能會引起一些問題,我將稍後解釋。

假設兩個或更多的線程對下面這個包含一個計數器的共享變量擁有訪問權限:

public class SharedObject {
    public int counter = 0;
}

再次假設,只有Thread1會增加 counter 變量的值,但是Thread1和Thread2都能在任意時刻讀取 counter 變量的值。

如果 couner 變量沒有聲明為volatile將無法保證在何時把CPU緩存中的值寫入主內存中。這意味著 counter 變量在CPU緩存中的值可能會與主內存中的值不一樣,如下所示:
技術分享
造成線程不能獲取變量最新值得原因為變量值沒有被其它線程及時寫回主內存中,這就是所謂的可見性問題。某個線程的更新對其它線程不可見。

counter 變量聲明為volatile

之後,所有對 counter 變量的寫操作會立即寫入主內存中,同樣,所有對 counter 變量的讀操作都會從主內存中讀取數據。下面的代碼塊展示了如何將 counter 變量聲明為volatile

public class SharedObject {
    public volatile int counter = 0;
}

因此定義一個volatile變量可以保證寫變量的操作對於其它線程可見。

Volatile先行發生原則

從Java5之後volatile關鍵字不僅能用於確保變量從主內存中讀取和寫入,事實上,volatile關鍵字還有如下作用:

  • 如果線程A寫入了一個volatile變量然後線程B讀取了這個相同的volatile變量,那麽所有在線程A寫之前對其可見的變量,在線程B讀取這個volatile之後也會對其可見。
  • 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;

由於線程A在寫操作volatile變量 sharedObject.counter 之前寫操作非volatile變量 sharedObject.nonVolatile ,因而當線程A寫操作變量 sharedObject.counter 後,變量 sharedObject.nonVolatilesharedObject.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) {
            //等待,不覆蓋已經存在的新對象
        }
        object = newObject;
        hasNewObject = true; //volatile寫入
    }

    public Object take(){
        while(!hasNewObject){ //volatile讀取
            //等待,不獲取舊的對象(或null對象)
        }
        Object obj = object;
        hasNewObject = false; //volatile寫入
        return obj;
    }
}

線程A隨時可能會通過調用 put() 方法增加對象,線程B隨時可能會通過調用 take() 方法獲取對象。只要線程A只調用 put() ,線程B只調用 take() ,這個 Exchanger 就可以通過一個volatile變量正常工作(排除synchronized代碼塊的使用)。

然而,JVM可能會重排序Java指令來優化性能,如果JVM可以通過不改變這些重排序指令的語義來實現此功能。如果JVM調換了 put()take() 中的讀和寫的指令,會發生什麽呢?如果 put() 真的像下面這樣執行會出現什麽情況呢?

while(hasNewObject) {
    //等待,不覆蓋已經存在的新對象
}
hasNewObject = true; //volatile寫入
object = newObject;

請註意此時對於volatile變量 hasNewObject 的寫操作會在新變量的實際設置前先執行,而這在JVM看來可能會完全合法。兩個寫操作指令的值不再依賴於對方。

但是,對於執行指令重排序可能會損害 object 變量的可見性。首先,線程B可能會在線程A對 object 真實的寫入一個值到object之前讀取到 hasNewObject 的值為true。其次,現在甚至不能保證什麽時候寫入 object 的新值會刷寫入主內存(好吧,下次線程A在其它地方寫入volatile變量。。。)

為了阻止上面所述的這種情況發生,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可能會重新排序前3條指令,只要它們都先發生於volatile寫指令(它們都必須在volatile寫指令之前執行)。

同樣的,JVM可能會重新排序最後3條指令,只要volatile寫指令先行發生於它們,這3條指令都不能被重新排序到volatile指令的前面。

這就是volatile先行發生原則的基本含義。

Volatile並不是萬能的

盡管volatile關鍵字確保了所有對於volatile變量的讀操作都是直接從主內存中讀取的,所有對於volatile變量的寫操作都是直接寫入主內存的,但仍有一些情況只定義一個volatile變量是不夠的。

在前面的場景中,線程1對共享變量counter寫入操作,聲明 counter 變量為volatile之後就能夠確保線程2總是可以看見最新的寫入值。

事實上,如果寫入該變量的值不依賴於它前面的值,多個線程甚至可以在寫入一個共享的volatile變量時仍然能夠持有在主內存中存儲的正確值。換句話解釋為,如果一個線程在寫入volatile共享變量時,不需要先讀取該變量的值以計算下一個值。

一旦一個線程需要首先讀取一個volatile變量的值,然後基於該值產生volatile共享變量的下一個值,那麽該volatile變量將不再能夠完全確保正確的可見性。在讀取volatile變量和寫入它的新值這個很短的時間間隔內,產生了一個 競爭條件 :多個線程可能會讀取volatile變量的相同值,然後產生新值並寫入主內存,這樣將會覆蓋互相的值。

這種多個線程同時增加相同計數器的場景正是volatile變量不適用的地方,接下來的部分進行了更詳細的解釋。

假設線程1讀取一個值為0的共享變量 counter 到它的CPU緩存中,將它加1但是並沒有將增加後的值寫入主內存中。線程2可能會從主內存中讀取同一個 counter 變量,其值仍然為0,同樣不將其寫入主內存中,就如下面的圖片所展示的那樣:
技術分享

線程1和線程2現在都沒有同步,共享變量 counter 的真實值應該是2,但是在每個線程的CPU緩存中,其值都為1,並且主內存中的值仍然是0。它成了一個爛攤子,即使這些線程終於它們對共享變量 counter 的計算值寫入到主內存中,counter 的值仍然是錯的。

Volatile的適用場景

就如在前面提到的那樣,如果兩個線程同時對一個共享變量進行讀和寫,那麽僅用volatile變量是不夠的。在這種情況下,你需要使用synchronized來確保關於該變量的讀和寫都是原子操作。讀或寫一個volatile變量時並不會阻塞其它線程對該變量的讀和寫。在這種情況下必須用synchronzied關鍵字來修飾你的關鍵代碼。

除了使用synchronzied之外,你也可以使用 java.util.concurrent 包中的一些原子數據類型,如 AtomicLong , AtomicReference等。

當只有一個線程對一個volatile變量進行讀寫而其它線程只讀取該變量時,volatile可以確保這些讀線程讀取到的是該變量的最新寫入值。如果不聲明該變量為volatile,則不能這些讀線程保證讀取的是最新寫入值。

Volatile關鍵字適用於32位變量和64位變量。

Volatile性能思考

由於volatile變量的讀和寫都是直接從主內存中進行的,相對於CPU緩存,直接對主內存進行讀寫代價更高, 訪問一個volatile變量也會阻止指令重新排序,而指令排序也是一個常用的性能增強技術。因此,你應該在只有當你確實需要確保變量可見性的時候才使用volatile變量

java學習之volatile