1. 程式人生 > >volatile機制詳述(可見性、原子性解釋)

volatile機制詳述(可見性、原子性解釋)

在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 publicclasswrongsingleton { privatestaticvolatilewrongsingleton _instance = null; privatewrongsingleton() {} publicstaticwrongsingleton getInstance() { if(_instance == null) { _instance = newwrongsingleton(); } return_instance; } }


下面的測試程式碼可以測試出是執行緒不安全的:

原因自然和上面的例子是一樣的。因為volatile保證變數對執行緒的可見性,但不保證原子性

附:正確執行緒安全的單例模式寫法:




1 2 3 4 5 6 7 8 9 @ThreadSafe publicclassSafeLazyInitialization { privatestaticResource resource; publicsynchronizedstaticResource getInstance() { if(resource == null) resource = newResource(); returnresource; } }


另外一種寫法:




1 2 3 4 5 @ThreadSafe publicclassEagerInitialization { privatestaticResource resource = newResource(); publicstaticResource getResource() { returnresource; } }


延遲初始化的寫法:




1 2 3 4 5 6 7 8 9 @ThreadSafe publicclassResourceFactory { privatestaticclassResourceHolder { publicstaticResource resource = newResource(); } publicstaticResource getResource() { returnResourceHolder.resource ; } }


二次檢查鎖定/Double Checked Locking的寫法(反模式)




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 publicclassSingletonDemo { privatestaticvolatileSingletonDemo instance = null;//注意需要volatile privateSingletonDemo() {   } publicstaticSingletonDemo getInstance() { if(instance == null) { //二次檢查,比直接用獨佔鎖效率高 synchronized(SingletonDemo .class){ if(instance == null) { instance = newSingletonDemo (); } } } returninstance; } }


為什麼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 publicclassAtomicLong extendsNumber implementsjava.io.Serializable { privatevolatilelongvalue; /** * Creates a new AtomicLong with the given initial value. * * @param initialValue the initial value */ publicAtomicLong(longinitialValue) { value = initialValue; } /** * Creates a new AtomicLong with initial value {@code 0}. */ publicAtomicLong() { }


其CAS原始碼核心程式碼為:




1 2 3 4 5 6 7 8 9 intcompare_and_swap (int* reg, intoldval, intnewval) { ATOMIC(); intold_reg_val = *reg; if(old_reg_val == oldval) *reg = newval; END_ATOMIC(); returnold_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在修改,那就繼續嘗試。所以這就保證了操作的原子性。

ConcurrencyCAS