為什么volatile不能保證原子性而Atomic可以?
在上篇《非阻塞同步算法與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)鍵字,就會告訴編譯器和JVM的內(nèi)存模型:這個(gè)變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值并使其最新值在所有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指令為:
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è)寫屏障會把這個(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è)字段的線程將會得到最新的值。2、在你寫入前,會保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的,因?yàn)閮?nèi)存屏障會把之前的寫入值都刷新到緩存。
volatile為什么沒有原子性?
明白了內(nèi)存屏障(memory barrier)這個(gè)CPU指令,回到前面的JVM指令:從Load到store到內(nèi)存屏障,一共4步,其中最后一步j(luò)vm讓這個(gè)最新的變量的值在所有線程可見,也就是最后一步讓所有的CPU內(nèi)核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。下面的測試代碼可以實(shí)際測試voaltile的自增沒有原子性:
private static volatile long _longVal = 0;
private static class LoopVolatile implements Runnable {
public void run() {
long val = 0;
while (val < 10000000L) {
_longVal++;
val++;
}
}
}
private static class LoopVolatile2 implements Runnable {
public void run() {
long val = 0;
while (val < 10000000L) {
_longVal++;
val++;
}
}
}
private void testVolatile(){
Thread t1 = new Thread(new LoopVolatile());
t1.start();
Thread t2 = new Thread(new LoopVolatile2());
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("final val is: " + _longVal);
}
Output:-------------
final val is: 11223828
final val is: 17567127
final val is: 12912109
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;
}
}
下面的測試代碼可以測試出是線程不安全的:
public class wrongsingleton {
private static volatile wrongsingleton _instance = null;
private wrongsingleton() {}
public static wrongsingleton getInstance() {
if (_instance == null) {
_instance = new wrongsingleton();
System.out.println("--initialized once.");
}
return _instance;
}
}
private static void testInit(){
Thread t1 = new Thread(new LoopInit());
Thread t2 = new Thread(new LoopInit2());
Thread t3 = new Thread(new LoopInit());
Thread t4 = new Thread(new LoopInit2());
t1.start();
t2.start();
t3.start();
t4.start();
while (t1.isAlive() || t2.isAlive() || t3.isAlive()|| t4.isAlive()) {
}
}
輸出:有時(shí)輸出"--initialized once."一次,有時(shí)輸出好幾次
原因自然和上面的例子是一樣的。因?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ū)懭耄娫创a:
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ù)嘗試。所以這就保證了操作的原子性。
posted on 2014-02-19 18:25 Mainz 閱讀(45696) 評論(11) 收藏 舉報(bào)

浙公網(wǎng)安備 33010602011771號