Java volatile 關鍵字深刻淺出

Java volitile關鍵字html

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

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

變量可見性問題多線程

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

在一個多線程的程序裏,若是線程操做一些非volatile的變量,爲了提升性能,每個線程均可能會從主內存複製變量值到CPU緩存。若是你的電腦的CPU數量多於一個,不一樣的線程可能會運行於不一樣的CPU上。這意味着不一樣的線程可能會把變量複製到不一樣CPU的緩存中,如圖所示:性能

 

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

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

 

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

若是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

相關文章
相關標籤/搜索