Jakob Jenkov多線程系列一一Java Volatile Keyword

Java Volatile Keyword

Java中的volatitle關鍵字的作用是使一個Java變量"被放置在主存區中",說得更確切一點是:每一個volatile變量只能從內存中被讀到,而不是從CPU緩存中,與此相對的是每一次寫入操作也會使變量被寫到內存中,而不是CPU緩存中。
在Java5.0以後的版本中,volatitle變量不僅僅用於讀寫操作了,下文會給出解釋。

The Java volatile Visibility Guarantee

Java中的volatitle關鍵字通過線程來保證一個變量的可見性,這聽起來可能有點玄幻。
在一個所有線程都不包含volatile關鍵字的程序中,每一個線程在操作變量時都能將其從主存區中複製到緩存。出於性能原因,如果你的電腦包含多個CPU,每個線程可能運行在不同的CPU上,這意味着每個線程可能複製同一個變量到不同的CPU緩存上,如下圖所示:

由圖中我們可以瞭解到,當虛擬機從主存區讀數據到CPU緩存,或者從CPU緩存寫數據到主存區中時,如果沒有volatitle語句意味着安全性得不到保證,並可能會導致一些問題,下面舉例說明:
當兩個或更多線程同時進入一個包含一個計數變量的類中時:
public class SharedObject {
public int counter = 0;
}
此時線程1在增加這個conunter變量,於此同時線程2可能在一次又一次的讀這個變量。
如果變量counter沒有volatitle關鍵字,那麼當counter的值從緩存被寫入到主存區時,就不能保證線程安全性。這也就意味着:在CPU緩存和主存區中的counter變量的值可能會不一樣,如下圖所示:
由於另一個線程沒有寫入主存區而導致當前線程沒有讀到變量當前值所導致的問題被稱爲"可見性問題",也就是說一個線程更新的值不能被其他線程看到。
解決辦法是給這個counter變量加上volatitle關鍵字,之後所有的寫入操作都會被立即寫入主存區,與此相對應的,所有的讀出操作也都直接從主存區中讀,如下所示:
public class SharedObject {

    public volatile int counter = 0;

}
通過設置volatitle關鍵字使得對其他寫入改變量的線程的可見性得到了保證。

The Java volatile Happens-Before Guarantee

自從Java5.0 volatile關鍵字出現以後,不只是保證了變量能夠被讀寫到主存區中,實際上還保證瞭如下功能:
1、如果同一時間線程A和B分別對一堆變量進行寫和讀操作,那麼執行寫入操作之前這些volatile變量對於A線程來說是可見的,與此相對,在執行讀出操作之後這些volatile變量對於線程B來說是可見的。
2、對volatile變量的讀寫操作不能被JVM重排序(JVM在不影響程序工作的情況下可能出於性能原因對指令進行重排),在volatile之前或之後的指令都有可能被重新排序,但是volatitle變量的讀寫指令不會被混到這些指令裏去,在volatitle讀寫之後的讀寫指令保證會在volatitle讀寫之後執行。
下面的部分需要更深層次的理解:
當一個線程寫入一個volatile變量時,不僅僅這個volatile變量本身被寫到主存區中,在此之前所有的被該線程改變過的變量都會被懟到主存區中去。
當一個線程讀到一個volatile變量時它也會讀到所有與這個volatitle一起被懟進來的變量。
舉個例子:
Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;
其中sharedObkect.nonVolatile是非volatile值,sharedObject.counter是volatile值。
因爲線程A在寫入sharedObkect.counter之前寫入了sharedObkect.nonVolatile的值,所以在A寫入sharedObkect.counter的同時sharedObkect.nonVolatile與sharedObkect.counter都被寫入到了主存區中。
同理可得,在線程B中因爲counter先被讀到緩存中,所以當B讀到sharedObkect.nonVolatile時可以看到該變量被A改動的值(可見性)。
開發者可以使用這個擴展的可見性保證技巧來優化線程間變量的可見性,只給一小部分變量加上volatile,而不是給每個變量都加上volatile。
下面是一個體現該原理的交換機類:
public class Exchanger {

    private Object   object       = null;
    private volatile boolean 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()方法,在這種情況下只能通過volatitle變量來保證該類運作正常。(同步鎖並不能做到這一點)。
因爲JVM可能出於性能考慮對指令進行重排序,而同步鎖並不能保證這種情況下指令的執行順序,設想一下這種情況下put()和take()方法的執行順序?
如果put()方法的執行情況如下所示:
while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
這裏要注意到volatitle變量hasNewObject是在設置新的object值之前執行的,對於JVM來說這段代碼是沒問題的,因爲這兩個寫入指令互不影響。

然而,對指令的重排可能會影響object變量的可見性。首當其衝的是線程B可能會在線程A給object設置新的值之前看到hasNewObeject已經被設置爲true,這樣B就執行不下去了。其次,也沒有能保證被寫到object的新的值在什麼時候會被flushback到主存區中。

爲了防止以上情況的發送,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關鍵字的寫入操作之前執行)

在volatile寫入指令執行之後,JVM可能會對剩下的三條指令進行重排。剩下的三條絕對不會在volatile寫入操作執行之前被重排。

以上就是"出現順序決定執行順序"特性的基本含義。

volatile is Not Always Enough

即使volatile關鍵字保證了一定直接從主存區中讀數據,一定寫入數據到主存區中,但是仍然有些情況是僅僅定義volatile變量不夠解決的。
在前面解釋的情況中,線程1寫入數據到共享的counter變量中,聲明volatile變量足以保證counter變量的改動會被線程2看到。
但事實上,多線程能夠同時對一個volatile數據進行寫入操作,並且能夠保證正確的值被存儲到主存區中,只要這個被寫入到該變量的新值是不依賴以前的值的。換句話說,這種情況是:如果一個線程寫入一個值到共享的volatile變量中,並不需要知道它上一個值是什麼。
如果一個線程需要首先讀取一個volatile變量的值,並且基於該值爲共享volatile量生成一個新的值,在這種情況下一個volatile變量就不足以保證可見性了當多個線程讀或寫到一個相同的volatile變量,爲了保證可見性而產生的在很短間隔內的對volatile的讀寫操作會產生一種競爭。
多個線程同時執行一個計數器的遞增正是一種這樣的情況,一個volatile變量是不夠的,以下部分將更詳細地解釋這種情況。
想象一下:當線程1讀到變量counter的值爲0,並讀到緩存中,對其進行增加1的操作,而此時這個改變還沒有寫入到主存區。線程2能夠讀到的counter的值在主存區中還仍然是0,於是線程2又將這個值讀到緩存中,進行+1操作,並且也還沒有寫入到主存區中去。情況如下圖所示:
線程1和線程2幾乎是同步執行的,此時真正的counter的值應該是2,然而每一個線程帶有的緩存中的該變量的值都是1,主存區中的值仍然是0,真是GG啊!
即使最終兩個線程都將各自的值寫入了主存區中,結果也是錯的。

When is volatile Enough?

正如我之前所提到的,如果兩個線程同時對一個變量進行讀寫操作,那麼此時volatile關鍵字就不夠了,需要和synchronized關鍵字去保證其原子性。
對volatile變量的讀或者寫並不限制線程讀寫。爲了實現這一點你必須用synchronized關鍵字在特定位置。
同樣爲了實現這種同步鎖的效果你也能使用java.util.concurrent package包中的一些原子類,例如AtomicLong或者AtomicReference或者其他的。
當一個線程對一個volatile變量進行讀寫操作,而其他線程只進行讀操作時,此時進行讀操作的線程被保證能看到最新的寫入到該volatile參數的值,如果不使用volatile參數,這將不能得到保證。

Performance Considerations of volatile


因爲volatile關鍵字會導致該變量讀寫在主存區中,比起普通的存到緩存區是更加消耗性能的。
通過設置volatile關鍵字防止指令重排也是提高性能的一種手段。
所以,你應該只在必須要可見性的時候才設置volatile關鍵字,不能濫用。



*********************************************************************************分割線**************************************************************
第一次發譯文,有很多地方翻譯得可能不是很透徹,這裏附上原文地址:http://tutorials.jenkov.com/java-concurrency/volatile.html
希望各位大牛能夠指出以使我能夠改正錯誤,謝謝!

發佈了70 篇原創文章 · 獲贊 78 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章