1. 程式人生 > 程式設計 >Java原子操作CAS原理解析

Java原子操作CAS原理解析

一、CAS(Compare And Set)

Compare And Set(或Compare And Swap),CAS是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,CAS操作包含三個運算元——記憶體位置(V)、預期原值(A)、新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。

​ 在java中可以通過鎖和迴圈CAS的方式來實現原子操作。Java中 java.util.concurrent.atomic包相關類就是 CAS的實現,atomic包裡包括以下類:

AtomicBoolean 可以用原子方式更新的boolean值。
AtomicInteger 可以用原子方式更新的int值。
AtomicIntegerArray 可以用原子方式更新其元素的int陣列。
AtomicIntegerFieldUpdater 基於反射的實用工具,可以對指定類的指定volatile int欄位進行原子更新。
AtomicLong 可以用原子方式更新的long值。
AtomicLongArray 可以用原子方式更新其元素的long陣列。
AtomicLongFieldUpdater 基於反射的實用工具,可以對指定類的指定volatile long欄位進行原子更新。
AtomicMarkableReference AtomicMarkableReference維護帶有標記位的物件引用,可以原子方式對其進行更新。
AtomicReference 可以用原子方式更新的物件引用。
AtomicReferenceArray 可以用原子方式更新其元素的物件引用陣列。
AtomicReferenceFieldUpdater<T,V> 基於反射的實用工具,可以對指定類的指定volatile欄位進行原子更新。
AtomicStampedReference AtomicStampedReference維護帶有整數“標誌”的物件引用,可以用原子方式對其進行更新。

二、AtomicInteger

AtomicInteger可以用原子方式更新的 int 值。AtomicInteger 可用在應用程式中(如以原子方式增加的計數器),並且不能用於替換 Integer。但是,此類確實擴充套件了 Number,允許那些處理基於數字類的工具和實用工具進行統一訪問。 我們拿 AtomicInteger為例來學習下 CAS操作是如何實現的。

通常情況下,在 Java中,i++等類似操作並不是執行緒安全的,因為 i++可分為三個獨立的操作:獲取變數當前值,為該值+1,然後寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作時“原子性”的。但是利用加鎖的方式來實現該功能的話,程式碼將非常複雜及難以維護,如:

synchronized (lock) { 
  i++; 
} 

相關類中還需要增加 Object lock等額外標誌,這樣就帶來了很多麻煩,增加了很多業務無關程式碼,給開發與維護帶來了不便。
​ 然而利用 atomic包中相關型別就可以很簡單實現此操作,以下是一個計數程式例項:

public class Counter {
  private AtomicInteger ai = new AtomicInteger();
  private int i = 0;
 
  public static void main(String[] args) { 
    final Counter cas = new Counter(); 
    List<Thread> threads = new ArrayList<Thread>();
    // 新增100個執行緒 
    for (int j = 0; j < 100; j++) { 
      threads.add(new Thread(new Runnable() {
        public void run() { 
          // 執行100次計算,預期結果應該是10000 
          for (int i = 0; i < 100; i++) { 
            cas.count(); 
            cas.safeCount(); 
          } 
        } 
      })); 
    } 
    //開始執行 
    for (Thread t : threads) {
      t.start(); 
    } 
    // 等待所有執行緒執行完成 
    for (Thread t : threads) {
      try { 
        t.join(); 
      } catch (InterruptedException e) { 
        e.printStackTrace(); 
      } 
    } 
    System.out.println("非執行緒安全計數結果:"+cas.i); 
    System.out.println("執行緒安全計數結果:"+cas.ai.get()); 
  } 
 
  /** 使用CAS實現執行緒安全計數器 */ 
  private void safeCount() { 
    for (;;) { 
      int i = ai.get(); 
      // 如果當前值 == 預期值,則以原子方式將該值設定為給定的更新值 
      boolean suc = ai.compareAndSet(i,++i); 
      if (suc) { 
        break; 
      } 
    } 
  } 
  /** 非執行緒安全計數器 */ 
  private void count() { 
    i++; 
  } 
} 
/**
非執行緒安全計數結果:9942
執行緒安全計數結果:10000
*/

其中非執行緒安全計數器所計算的結果每次都不相同且不正確,而執行緒安全計數器計算的結果每次都是正確的。

三、存在的問題

CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、迴圈時間長開銷大、只能保證一個共享變數的原子操作。

ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的 atomic包裡提供了一個類AtomicStampedReference 來解決ABA問題。這個類的 compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

迴圈時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。 

只能保證一個共享變數的原子操作:當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。