為什么volatile不能保證原子性?


為什么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的自增沒有原子性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
     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單例模式實現

這是一段線程不安全的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在修改,那就繼續嘗試。所以這就保證了操作的原子性。

ConcurrencyCAS

 

 

轉載自:http://www.cnblogs.com/Mainz/p/3556430.html#


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM