1. 程式人生 > >Java volatile 關鍵字完全解釋 - 附例子

Java volatile 關鍵字完全解釋 - 附例子

volatil vol 由於 同時 網址 變量 內存 大於 分享圖片

Java volitile關鍵字

Java volatile 關鍵字用來標記一個Java變量為“存儲於主內存”。更準確地說是,每一次針對volatile變量的讀操作將會從主內存讀取而不是從CPU的緩存讀取;每一次針對volatile變量的寫操作都會寫入主內存,而不僅僅是寫入CPU緩存。

實際上,從Java 5開始,volatile關鍵字除了保證從主內存讀寫volatile變量以外,還保證了其他的一些東西。我將會在後面的部分進行解釋。

變量可見性問題

Java volatile關鍵字保證變量值的變化在多個線程間的可見性。這個描述有些抽象,所以讓我詳細的解釋一下。

在一個多線程的程序裏,如果線程操作一些非volatile的變量,為了提高性能,每一個線程都可能會從主內存復制變量值到CPU緩存。如果你的電腦的CPU數量多於一個,不同的線程可能會運行於不同的CPU上。這意味著不同的線程可能會把變量復制到不同CPU的緩存中,如圖所示:

技術分享圖片

對於使用非volatile的變量,Java虛擬機(JVM)將不會保證何時從主內存讀取數據到CPU緩存,也不會保證何時把CPU緩存的數據寫回到主內存。這將會造成一些問題。後面我將會詳細解釋。

設想以下情形,有兩個或者兩個以上的線程可以訪問到一個包含了一個計數器的共享對象:

技術分享圖片

再設想一下,只有線程1增加counter變量,但是線程1和線程2會不時的讀取counter變量。

如果counter變量沒有被聲明為volatile,counter變量的值將不會被保證何時才能從CPU緩存寫回到主內存。這意味著counter變量在CPU緩存中的值可能和主內存中的值不一樣。這個情形如圖所示:

技術分享圖片

因一個線程還沒有把變量的值寫回主內存,其他線程不能讀取到這個變量最新的值的問題被稱為“可見性”問題。一個線程的更改對於其他線程不可見。

Java volatile可見性保證

Java volatile關鍵字的目標就是解決變量的可見性問題。聲明了帶volatile的counter變量,所有對counter的寫操作將會理解被寫回到主內存。所有對counter變量的讀操作也會從主內存讀取。

以下是帶了volatile的counter的聲明:

技術分享圖片

聲明一個變量為volatile由此可以保證其他線程對該變量的寫操作的可見性。

在上面的情形中,一個線程(線程1)修改了counter,另一個線程(線程2)讀取了counter(但是從不會修改它),聲明counter變量為volatile足以保證線程2對於針對counter變量寫操作的可見性。

但是如果線程1和線程2都修改了counter的值,那麽僅僅聲明counter變量為volatile是不夠的。後面會詳細解釋。

完全的volatile可見性保證

實際上,Java volatile的可見性保證超出了volatile變量本身。可見性保證如下:

  • 如果線程A寫入volatile變量,而後線程B讀取同一個volatile變量,那麽所有在線程A寫入volatile變量之前對線程A可見的變量(譯者:不一定是volatile變量)將會在線程B讀取此volatile變量後對線程B可見。
  • 如果線程A讀取了一個volatile變量,那麽所有的當線程A讀取此volatile變量時對線程A可見的變量(譯者:不一定是volatile變量)將也會從主內存讀取。

讓我們來看一個代碼的例子:

技術分享圖片

update()方法寫入三個變量,其中只有days是volatile的。

完全的volatile可見性保證的意思是,當一個值被寫入days的時候,所有對此線程可見的變量們將也會被寫入主內存。也就是說,當一個值被寫入days的時候,years和months的值也會被寫入主內存。

當讀取years,months和days的值的時候,你可以這樣寫:

技術分享圖片

註意totalDays()方法一上來就先讀取days的值到total變量。當讀取days的值,months和years也會從主內存讀取。因此,使用上面的讀取順序,可以確保讀取到days,months和years的最新的值。

指令重排序帶來的挑戰

由於性能方面的原因,JVM和CPU只要能夠保證指令的語義保持一致,是可以對指令進行重新排序的。比如下面的代碼:

技術分享圖片

這些指令可以按照下面的順序重新排序,但是並沒有喪失掉程序原來的語義:

技術分享圖片

但是當一些變量中的一個為volatile變量時,指令重排帶來了挑戰。讓我們看一下前面例子中的MyClass類。

技術分享圖片

當update()方法寫入值到days的時候,years和months的新寫入值也會寫入主內存中。但是如果JVM像下面一樣重排了這些指令的順序怎麽辦:

技術分享圖片

當days變量更改時,months和years的值仍然會寫入主內存,但是這時新的值還沒有寫入months和years。新的值因此沒有適當的對其他線程可見。重新排序的指令的語義發生了改變。

Java針對此問題有一個解決方案。我們將會在下一節看到。

Java volatile “之前發生(Happens-Before)”保證

為了應對指令重排序帶來的挑戰,除了可見性保證,Java volatile關鍵字還提供了“之前發生”(Happens-Before)保證。之前發生保證:

  • 如果對其他一些變量的讀取/寫入操作原本就發生在對一個volatile變量的寫入之前,那麽對這些其他變量的讀取/寫入操作不能被重排序到對這個volatile變量的寫入之後。在寫入一個volatile變量之前的讀取/寫入操作被保證在寫入volatile變量“之前發生”。註意,下面的情況依然可能發生:原本就發生在對一個volatile變量寫入之後的對其他變量的讀取/寫入操作可能會被重排序到對volatile變量的寫入之前。只是反過來不可能。從之後到之前是允許的,但是從之前到之後不允許。
  • 如果對其他一些變量的讀取/寫入操作原本就發生在對一個volatile變量的讀取之後,那麽對這些其他變量的讀取/寫入操作不能被重排序到對這個volatile變量的讀取之前。註意,下面的情況依然可能發生:原本就發生在對一個volatile變量的讀取之前的對其他變量的讀取操作可能會被重新排序到對volatile變量的讀取之後。只是反過來不可能。從之前到之後是允許的,從之後到之前不允許。

以上的“之前發生”保證確保了volatile關鍵字對於可見性的保證。

volatile並不總是足夠的

雖然volatile關鍵字保證所有讀取volatile變量都從主內存讀取,並且所有寫入volatile變量都直接寫入主內存,但是僅僅聲明變量為volatile仍然不夠的情形依然存在。

在上面的情形中,只有線程1會寫入共享的counter變量,聲明counter為volatile可以足夠保證線程2總是能看到最新的寫入值。

實際上,如果新寫入的變量值不依賴於變量的前值(換句話說就是,一個線程不需要通過先讀取一個變量的值進而計算出新值),甚至多個線程可以寫入一個共享的volatile變量,但是主內存中的變量值也是正確的。

當一個線程需要首先讀取volatile變量的值,然後基於這個值生成這個共享的volatile變量的新值,僅僅聲明變量為volatile就不再能夠保證變量的正確的可見性了。

從讀取volatile變量到對此變量寫入新值的這段很短的時間,會產生競爭狀況。競爭狀況在這裏是指多個線程可能讀取到volatile變量相同的值,為這個變量生成新值,當把值寫回主內存時多個線程覆蓋掉彼此的值。

多個線程同時增加同一個counter的值正是這樣一個volatile變量不足以保證正確性的情形。後續將會詳細解釋這種情形。

假設線程1讀取共享的counter變量值0到CPU緩存,增加這個值為1但是還沒有把更改的值寫回到主內存。線程2可能讀取到此counter變量的值也是0,並放到它自己的CPU緩存。線程2接下來可能也增加counter的值為1,並且也不把更新的值寫回到主內存。這個情形如圖所示:

技術分享圖片

線程1和線程2實際上已經不同步了。這個共享的counter變量的值本應該是2,但是每一個線程在他們的CPU緩存中的值都是1,而主內存中的值還依然是0。這已經亂了。即使兩個線程把值從CPU緩存寫入主內存,值還是錯的。

什麽時候volatile是足夠的

正如我前面說的,如果兩個線程會同時讀取寫入一個共享的變量,僅僅聲明變量為volatile是不夠的。這種情形你需要使用synchronized關鍵字來保證從讀取到寫入變量的原子性。讀取或者寫入一個volatile變量並不會阻塞其他線程的讀寫。如果想阻塞,你必須在臨界區周圍使用synchronized關鍵字。

作為synchronized關鍵字的替代,你也可以使用java.util.concurrent包中的原子數據類型,比如AtomicLong或者AtomicReference等。

如果只有一個線程會讀取和寫入volatile變量,而其他的線程只會讀取變量的值,那麽讀取值的線程將被保證能讀到最新寫入volatile變量的值。如果變量不聲明為volatile,這將不能被保證。

volatile關鍵字支持32位和64位的變量。

volatile與性能

對volatile變量的讀寫會造成讀寫發生於主內存。對主內存讀寫的開銷遠遠大於對CPU緩存的開銷。對volatile變量的訪問也會導致指令不能被重排序,而重排序是一種常規的提高性能的技術。因此你應該只在真正需要保證變量可見性的時候使用volatile變量。

譯者總結:

  1. volatile用於保證在多CPU環境中多線程對於共享變量值變化的可見性
  2. 可見性問題是由CPU緩存造成的
  3. 如果多個變量都需要解決可見性問題,不一定所有變量都需要聲明為volatile。以下情形也可以保證可見性:

只聲明一個變量為volatile,然後讀取的時候最先讀取volatile變量,寫入的時候最後寫入volatile變量。

英文網址:

http://tutorials.jenkov.com/java-concurrency/volatile.html

Java volatile 關鍵字完全解釋 - 附例子