標籤:沒有 修改 tor atom 儲存 抽象 .com date lda
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變數。
譯者總結:
- volatile用於保證在多CPU環境中多線程對於共用變數值變化的可見度
- 可見度問題是由CPU緩衝造成的
- 如果多個變數都需要解決可見度問題,不一定所有變數都需要聲明為volatile。以下情形也可以保證可見度:
只聲明一個變數為volatile,然後讀取的時候最先讀取volatile變數,寫入的時候最後寫入volatile變數。
英文網址:
http://tutorials.jenkov.com/java-concurrency/volatile.html
Java volatile 關鍵字完全解釋 - 附例子