(轉(zhuǎn)載)為什么volatile不能保證原子性而Atomic可以?

原文:https://www.cnblogs.com/Mainz/p/3556430.html

在上篇《非阻塞同步算法與CAS(Compare and Swap)無鎖算法》中講到在Java中l(wèi)ong賦值不是原子操作,因?yàn)橄葘?2位,再寫后32位,分兩步操作,而AtomicLong賦值是原子操作,為什么?為什么volatile能替代簡單的鎖,卻不能保證原子性?這里面涉及volatile,是java中的一個(gè)我覺得這個(gè)詞在Java規(guī)范中從未被解釋清楚的神奇關(guān)鍵詞,在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.

意思就是說,如果一個(gè)變量加了volatile關(guān)鍵字,就會(huì)告訴編譯器和JVM的內(nèi)存模型:這個(gè)變量是對所有線程共享的、可見的,每次jvm都會(huì)讀取最新寫入的值并使其最新值在所有CPU可見。volatile似乎是有時(shí)候可以代替簡單的鎖,似乎加了volatile關(guān)鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性)。這不是互相矛盾嗎?

不要將volatile用在getAndOperate場合,僅僅set或者get的場景是適合volatile的

不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖),僅僅set或者get的場景是適合volatile的

volatile沒有原子性舉例:AtomicInteger自增

例如你讓一個(gè)volatile的integer自增(i++),其實(shí)要分成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

|

注意最后一步是內(nèi)存屏障。

什么是內(nèi)存屏障(Memory Barrier)?

內(nèi)存屏障(memory barrier)是一個(gè)CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操作執(zhí)行的順序; b) 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)。編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對指令重排序,使性能得到優(yōu)化。插入一個(gè)內(nèi)存屏障,相當(dāng)于告訴CPU和編譯器先于這個(gè)命令的必須先執(zhí)行,后于這個(gè)命令的必須后執(zhí)行。內(nèi)存屏障另一個(gè)作用是強(qiáng)制更新一次不同CPU的緩存。例如,一個(gè)寫屏障會(huì)把這個(gè)屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值,而不用考慮到底是被哪個(gè)cpu核心或者哪顆CPU執(zhí)行的。

內(nèi)存屏障(memory barrier)和volatile什么關(guān)系?上面的虛擬機(jī)指令里面有提到,如果你的字段是volatile,Java內(nèi)存模型將在寫操作后插入一個(gè)寫屏障指令,在讀操作前插入一個(gè)讀屏障指令。這意味著如果你對一個(gè)volatile字段進(jìn)行寫操作,你必須知道:1、一旦你完成寫入,任何訪問這個(gè)字段的線程將會(huì)得到最新的值。2、在你寫入前,會(huì)保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會(huì)把之前的寫入值都刷新到緩存。

volatile為什么沒有原子性?

明白了內(nèi)存屏障(memory barrier)這個(gè)CPU指令,回到前面的JVM指令:從Load到store到內(nèi)存屏障,一共4步,其中最后一步j(luò)vm讓這個(gè)最新的變量的值在所有線程可見,也就是最后一步讓所有的CPU內(nèi)核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會(huì)丟失。下面的測試代碼可以實(shí)際測試voaltile的自增沒有原子性:

+ View Code

volatile沒有原子性舉例:singleton單例模式實(shí)現(xiàn)

這是一段線程不安全的singleton(單例模式)實(shí)現(xiàn),盡管使用了volatile:

public class wrongsingleton {

private static volatile wrongsingleton _instance = null``;

private wrongsingleton() {}

public static wrongsingleton getInstance() {

if (_instance == null``) {

_instance = new wrongsingleton();

}

return _instance;

}

}

|

下面的測試代碼可以測試出是線程不安全的:

+ View Code

原因自然和上面的例子是一樣的。因?yàn)?strong>volatile保證變量對線程的可見性,但不保證原子性。

附:正確線程安全的單例模式寫法:

@ThreadSafe

public class SafeLazyInitialization {

private static Resource resource;

public synchronized static Resource getInstance() {

if (resource == null``)

resource = new Resource();

return resource;

}

}

另外一種寫法:

@ThreadSafe

public class EagerInitialization {

private static Resource resource = new Resource();

public static Resource getResource() { return resource; }

}

延遲初始化的寫法:

@ThreadSafe

public class ResourceFactory {

private static class ResourceHolder {

public static Resource resource = new Resource();

}

public static Resource getResource() {

return ResourceHolder.resource ;

}

}

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

public class SingletonDemo {

private static volatile SingletonDemo instance = null``;``//注意需要volatile

private SingletonDemo() { }

public static SingletonDemo getInstance() {

if (instance == null``) { //二次檢查,比直接用獨(dú)占鎖效率高

synchronized (SingletonDemo .``class``){

if (instance == null``) {

instance = new SingletonDemo ();

}

}

}

return instance;

}

}

|

為什么AtomicXXX具有原子性和可見性?

就拿AtomicLong來說,它既解決了上述的volatile的原子性沒有保證的問題,又具有可見性。它是如何做到的?當(dāng)然就是上文《非阻塞同步算法與CAS(Compare and Swap)無鎖算法》提到的CAS(比較并交換)指令。 其實(shí)AtomicLong的源碼里也用到了volatile,但只是用來讀取或?qū)懭?,見源碼:

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源碼核心代碼為:

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;

}

虛擬機(jī)指令為:

mov 0xc``(%r11),%eax ; Load

mov %eax,%r8d

inc %r8d ; Increment

lock cmpxchg %r8d,``0xc``(%r11) ; Compare and exchange

|

因?yàn)镃AS是基于樂觀鎖的,也就是說當(dāng)寫入的時(shí)候,如果寄存器舊值已經(jīng)不等于現(xiàn)值,說明有其他CPU在修改,那就繼續(xù)嘗試。所以這就保證了操作的原子性。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容