深入理解Java多線程中的volatile關鍵字

  • Java 的 volatile關鍵字對可見性的保證
  • Java 的 volatile關鍵字在保證可見性之前的所做的事情
  • 為什么volatile關鍵字有時候也不是足夠的
  • 什么時候volatile足夠了
  • volatile關鍵字對效率的影響

Java關鍵字用于將一個變量標記為“存儲在內存中的變量”。更準確的說,意思就是每一次對volatile標記的變量進行讀取的時候,都是直接從電腦的主內存進行的,而不是從cpu的cache中,而且每個對volatile變量的寫入操作,都會被直接寫入到主存里,而不是只寫到cache里。

實際上,從java5開始,volatile關鍵字就不僅僅是保證volatile變量從主存讀寫,筆者會在后面詳細討論這個問題。

Java 的 volatile關鍵字對可見性的保證

Java的volatile關鍵字可以保證變量的可見性。說起來很簡單,但具體是什么意思呢?

在多線程的應用程序中,線程操作非volatile的變量,為了更快速的執(zhí)行程序,每個線程都會將變量從主存復制到cpu的cache中。如果你的電腦有多個cpu,每個線程都在不同的cpu上運行,這就意味著,每個線程將變量的值復制到不同的cpu的cache上,就像下面這個圖所表明:

Paste_Image.png

如果變量沒有聲明為volatile,那么就無法知道,變量什么時候從主存中讀取到cpu的cache中,有什么時候從cache中寫回到主存中。這就可能造成很多潛在的問題:

假設一種情況,多個線程同時持有一個共享對象的引用,這個對象包括一個counter變量:

public class SharedObject {

    public int counter = 0;

}

假設這種情況,只有線程1自增了這個counter變量,但是線程1和線程2可能隨時讀取這個counter變量。如果這個counter變量沒有被聲明為volatile,那么就無法確認,什么時候counter的變量的值會從cpu的cache中寫回到主存中,這就意味著,counter變量的值在cpu的cache中的值可能和主存中不一樣,如下圖所示:

Paste_Image.png

這個線程的問題無法及時的看到變量的最新的值,因為可能這個變量還沒有被另一個線程寫回到主存中。所以一個線程對一個變量的更新對其他的線程是不可見的。這就是我們最初提出的線程的可見性問題。

通過將一個變量聲明為volatile,那么所有對這個變量寫操作會被直接寫回到主內存中,所以這對線程都是可見的。而且,所有對這個變量的讀取操作,也會直接從主存中讀取,下面說明了如何聲明一個voaltile變量:

public class SharedObject {

    public volatile int counter = 0;

}

** 將一個變量聲明為volatile就可以保證寫操作,其他線程對這個變量的可見性 **

Java 的 volatile關鍵字在保證可見性之前的所做的事情

從java5開始,volatile關鍵字不僅可以保證變量直接從主內存中讀取,還有一下作用:

  • 如果線程A對一個volatile變量進行寫操作,線程B隨后讀取同一個volatile值,那么在線程將變量寫操作完成之后的所有變量對線程A和B都是可見的。
  • 那些操作volatile變量的讀寫指令的順序無法被JVM改變(JVM有時候為了效率會改變變量讀寫順序,只要JVM判斷改變順序對程序沒有影響的話)。

上面兩段話不是很理解,我們接下來進行一個更細致的說明:

當一個線程對一個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進行寫操作之前,先對sharedObject.nonVolatile變量進行寫操作,所以當線程A要將volatile的sharedObject.counter寫回到主存時,這兩個變量都會被寫回到主存中。

同理,線程B在讀取volatile變量到sharedObject.counter的時候,兩個變量sharedObject.counter and sharedObject.nonVolatile所以線程讀取變量sharedObject.nonVolatile就會看到他被線程A改變后的值。

開發(fā)者可以利用這個擴展的可見性去放大線程間的變量可見性,不需要將每一個變量都聲明為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方法將objects put進去,線程B可能會調用take方法將object拿出來。這個類可以正常工作,只要我們使用一個volatile變量即可(不使用同步語句),只要只有線程A調用put,只有線程B調用take。

然后,JVM有時候為了提高效率,可能會改變指令執(zhí)行的順序,只要JVM判斷這樣做不改變指令的語義,那么就有可能改變指令的順序。那么如果JVM改變了指令的執(zhí)行順序會發(fā)生什么呢?put方法可能會像下面這樣執(zhí)行:

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

我們觀察到,現在對于volatile的hasNewObject 操作在object = newObject;之前執(zhí)行,這說明,object還沒有真正被賦值新對象,但是hasNewObject 已經先變?yōu)閠rue了。對于JVM來說,這種交換是完全有可能的。因為這兩個write的指令彼此不是互相依賴的。

但是這樣交換順序之后可能會對object變量的可見性產生不好的影響。首先,線程B可能會在線程A真正給object寫入一個新值之前,就看到hasNewObject 變?yōu)閠rue。
另一方面,我們無法確保object什么時候會被真正寫入到主內存中。

為了防止上面這種情況的發(fā)生,volatile關鍵字就提出了一種“happens before guarantee”,這可以保證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的寫指令之前發(fā)生(就是說他們必須在volatile的寫指令之前發(fā)生)。
同理,JVM也可能改變后三個指令的順序,只要他們在volatile的寫指令之后發(fā)生。

這就是對于Java的 volatile happens before guarantee.的最基本的理解

Volatile有時候也是不夠的

雖然volatile可以保證讀取操作直接從主內存中的讀取,寫操作直接寫到內存中,但仍然存在一些情況下,光使用volatile關鍵字是不夠的。

在之前的舉例的程序中,只有一個線程在向共享變量寫入數據的時候,聲明為volatile,另一個線程就可以一直看到最新被寫入的值。

實際上,只要新值不依賴舊值的情況下,多個線程同時向共享的volatile變量里寫入數據時,仍然能在主內存中得到正確的值。換句話說,就是這個volatile變量值更新的時候,不需要先讀取出他以前的值才能得到下一個值。

只要一個線程需要先讀取一個voaltile變量,然后必須基于他的值才能產生新的值,那么volatile關鍵字就不再能保證變量的可見性了。在讀取變量和寫入變量的時候,存在一個短的時間間隙,這就會造成,多個線程可能會在這個間隙讀取同一個值,產生一個新值,然后寫入到主內存中,將其他線程對值的改變給覆蓋了。

所以常見的情況就是如果一個volatile變量在進行自增或者自減操作,那么這時候使用volatile就可能出問題。
接下來我們更深入的討論這個問題,假設線程1讀取一個共享的counter變量到cpu的cache中,此時他的值是0,然后給它自增加一,但是還沒有寫到主存中,所以主存中還是1,線程2也能夠讀取同一個counter變量,而這個變量讀取的時候還是0,在他自己的cpucache中,這樣就出現問題了:

Paste_Image.png

線程1和線程2實際上是不同步的。共享變量counter的真實值實際上應該為2,因為被加了兩次,但是每個線程在自己的cache上存的值是1,而且在主存中這個值仍然是0,這就變得很混亂。即使線程最后將值寫回到主存中,但最后的值也是不正確的。

什么時候volatile足夠了

前文中提到,如果兩個線程都在對volatile變量進行讀寫操作,那么僅僅使用volatile關鍵字是遠遠不夠的。你需要使用synchronize關鍵字,來保證讀寫操作的原子性。
但如果是只有一個線程在讀寫volatile變量,另外的多個線程僅僅是讀取這個變量的話,那么這就可以保證,其他讀線程所看到的變量值都是最新的。volatile關鍵字可以使用在32位或者64位的變量上。

volatile關鍵字對效率的影響

讀寫一個volatile變量的時候,會導致變量直接在主存中讀寫,顯然,直接從主存中讀寫速度要比從cache中來得慢。另一方面,操作volatile變量的時候不能改變指令的執(zhí)行順序,這一定程度上也會影響讀寫的效率。所以,只有我們需要確保變量可見性的時候,才會使用volatile關鍵字。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 前言 今天介紹下volatile關鍵字,volatile這個關鍵字可能很多朋友都聽說過,或許也都用過。在Java ...
    嘟爺MD閱讀 1,359評論 7 27
  • Java的關鍵字 volatile 用于將變量標記為“存儲于主內存中”。更確切地說,對 volatile 變量的每...
    holysu閱讀 5,834評論 0 13
  • 《體驗》 送葬的隊伍從東到西 在坡馬的中心路上走過去 百家姓上的張、王、李、趙…… 都在走過去。我感到路在縮短 因...
    溪小石吳索衛(wèi)閱讀 108評論 1 1
  • 一 奧莉加·伊凡諾夫娜所有的朋友和熟人都出席了她的婚禮。 “你們瞧瞧:他是不是有點意思?”她對朋友們說,朝丈夫那邊...
    小團閱讀 1,015評論 0 0
  • 1.2016年中讓自己可以拿到機動車駕駛證(得花時間努力去練車) 2.閱讀1000本電子書(希望更好的提升自己) ...
    兔子彤閱讀 648評論 0 1

友情鏈接更多精彩內容