http://blog.chinaunix.net/uid-23629988-id-3166124.html
現在volatile的情況已經明瞭。volatile只提供了保證訪問該變量時,每次都是從內存中讀取最新值,並不會使用寄存器緩存該值——每次都會從內存中讀取。而對該變量的修改,volatile並不提供原子性的保證。那麼編譯器究竟是直接修改內存的值,還是使用寄存器修改都符合volatile的定義。所以,一句話,volatile並不提供原子性的保證。
http://www.cnblogs.com/Mainz/p/3556430.html#
在上篇《非阻塞同步算法與CAS(Compare and Swap)無鎖算法》中講到在Java中long賦值不是原子操作,因爲先寫32位,再寫後32位,分兩步操作,而AtomicLong賦值是原子操作,爲什麼?爲什麼volatile能替代簡單的鎖,卻不能保證原子性?這裏面涉及volatile,是java中的一個我覺得這個詞在Java規範中從未被解釋清楚的神奇關鍵詞,在Sun的JDK官方文檔是這樣形容volatile的:
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.
意思就是說,如果一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在所有CPU可見。volatile似乎是有時候可以代替簡單的鎖,似乎加了volatile關鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性)。這不是互相矛盾嗎?
不要將volatile用在getAndOperate場合,僅僅set或者get的場景是適合volatile的
不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖),僅僅set或者get的場景是適合volatile的。
volatile沒有原子性舉例:AtomicInteger自增
例如你讓一個volatile的integer自增(i++),其實要分成3步:1)讀取volatile變量值到local; 2)增加變量的值;3)把local的值寫回,讓其它的線程可見。這3步的jvm指令爲:
1
2
3
4
|
mov 0xc (%r10),%r8d
; Load inc
%r8d ; Increment mov
%r8d, 0xc (%r10)
; Store lock
addl $ 0x0 ,(%rsp)
; StoreLoad Barrier |
注意最後一步是內存屏障。
什麼是內存屏障(Memory Barrier)?
內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操作執行的順序; b) 影響一些數據的可見性(可能是某些指令執行後的結果)。編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。內存屏障另一個作用是強制更新一次不同CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。
內存屏障(memory barrier)和volatile什麼關係?上面的虛擬機指令裏面有提到,如果你的字段是volatile,Java內存模型將在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。這意味着如果你對一個volatile字段進行寫操作,你必須知道:1、一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。2、在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因爲內存屏障會把之前的寫入值都刷新到緩存。
volatile爲什麼沒有原子性?
明白了內存屏障(memory barrier)這個CPU指令,回到前面的JVM指令:從Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在所有線程可見,也就是最後一步讓所有的CPU內核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。下面的測試代碼可以實際測試voaltile的自增沒有原子性:
volatile沒有原子性舉例:singleton單例模式實現
這是一段線程不安全的singleton(單例模式)實現,儘管使用了volatile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class wrongsingleton
{ private static volatile wrongsingleton
_instance = null ; private wrongsingleton()
{} public static wrongsingleton
getInstance() { if (_instance
== null )
{ _instance
= new wrongsingleton(); } return _instance; } } |
下面的測試代碼可以測試出是線程不安全的:
原因自然和上面的例子是一樣的。因爲volatile保證變量對線程的可見性,但不保證原子性。
附:正確線程安全的單例模式寫法:
1
2
3
4
5
6
7
8
9
|
@ThreadSafe public class SafeLazyInitialization
{ private static Resource
resource; public synchronized static Resource
getInstance() { if (resource
== null ) resource
= new Resource(); return resource; } } |
另外一種寫法:
1
2
3
4
5
|
@ThreadSafe public class EagerInitialization
{ private static Resource
resource = new Resource(); public static Resource
getResource() { return resource;
} } |
延遲初始化的寫法:
1
2
3
4
5
6
7
8
9
|
@ThreadSafe public class ResourceFactory
{ private static class ResourceHolder
{ public static Resource
resource = new Resource(); } public static Resource
getResource() { return ResourceHolder.resource
; } } |
二次檢查鎖定/Double Checked Locking的寫法(反模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class SingletonDemo
{ private static volatile SingletonDemo
instance = null ; //注意需要volatile private SingletonDemo()
{ } public static SingletonDemo
getInstance() { if (instance
== null )
{ //二次檢查,比直接用獨佔鎖效率高 synchronized (SingletonDemo
. class ){ if (instance
== null )
{ instance
= new SingletonDemo
(); } } } return instance; } } |
爲什麼AtomicXXX具有原子性和可見性?
就拿AtomicLong來說,它既解決了上述的volatile的原子性沒有保證的問題,又具有可見性。它是如何做到的?當然就是上文《非阻塞同步算法與CAS(Compare and Swap)無鎖算法》提到的CAS(比較並交換)指令。 其實AtomicLong的源碼裏也用到了volatile,但只是用來讀取或寫入,見源碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class AtomicLong extends Number implements java.io.Serializable
{ private volatile long value; /** *
Creates a new AtomicLong with the given initial value. * *
@param initialValue the initial value */ public AtomicLong( long initialValue)
{ value
= initialValue; } /** *
Creates a new AtomicLong with initial value {@code 0}. */ public AtomicLong()
{ } |
其CAS源碼核心代碼爲:
1
2
3
4
5
6
7
8
9
|
int compare_and_swap
( int *
reg, int oldval, int newval) { ATOMIC(); int old_reg_val
= *reg; if (old_reg_val
== oldval) *reg
= newval; END_ATOMIC(); return old_reg_val; } |
虛擬機指令爲:
1
2
3
4
|
mov 0xc (%r11),%eax
; Load mov
%eax,%r8d inc
%r8d ; Increment lock
cmpxchg %r8d, 0xc (%r11)
; Compare and exchange |
因爲CAS是基於樂觀鎖的,也就是說當寫入的時候,如果寄存器舊值已經不等於現值,說明有其他CPU在修改,那就繼續嘗試。所以這就保證了操作的原子性。