Java並發之CAS詳解


一、前言 

首先我們要了解Java內存模型(Java Memory Model)。JMM就是一套規范,描述了Java線程對變量的訪問規則。

  在JVM中有一個main memory,而每個線程都有自己的working memory,一個線程對一個共享variable進行操作的時候,會先在自己的working memory里面建立一個copy,操作完成之后再寫入main memory,如果有多個線程同時操作同一個variable,就可能會出現不可預知的結果,所以線程安全就是為了避免這種情況的發生。
  volatile是不錯的機制,volatile關鍵字作用:1.volatile可以保證變量的可見性。2.保證有序性。(防止指令重排)。【當一個線程修改了共享變量的值,新的值會立刻同步到主內存當中。而其他線程讀取這個變量的時候,也會強制從主內存中拉取最新的變量值。】但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。在java中,確保線程安全的方法有兩種:一種是使用內置鎖(synchronized),一種是使用原子類(java.util.concurrent包下的),對於原子類,它所用的機制就是CAS機制


二、什么是CAS?

  CAS:Compare and Swap,即比較再交換。

  jdk5增加了並發包java.util.concurrent.*,其下面的類使用CAS算法實現了區別於synchronouse同步鎖的一種樂觀鎖。JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這是一種獨占鎖,也是是悲觀鎖。

  CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。
CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”CAS是通過無限循環來獲取數據的,若果在第一輪循環中,a線程獲取地址里面的值被b線程修改了,那么a線程需要自旋,到下次循環才有可能機會執行。

  通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然后使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。

  現代cpu提供了特殊的指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而compareAndSet()就是用這些代替了鎖定,compareAndeSet()是調用native方法來完成cpu指令的操作。 

來看看AutomicInteger的源碼

private volatile int value; //毫無疑問,沒有鎖的機制下,必須借助volatile保證線程間的數據可見性

public final int get(){
     return value;
}

// 來看看++i是怎么實現的:
public final int increamentAndGet(){
     for (;;){  // 無限循環來獲取數據
         int current = get();
         int next = current +  1 ;
         if (compareAndSet(current,next))
             return next;
       }
}

  由此可見,AtomicInteger.incrementAndGet的實現用了樂觀鎖技術,調用了類sun.misc.Unsafe庫里面的 CAS算法,用CPU指令來實現無鎖自增。所以,AtomicInteger.incrementAndGet的自增比用synchronized的鎖效率倍增。


三、CAS存在的問題

1. ABA 問題

  談到 CAS,基本上都要談一下 CAS 的 ABA 問題。CAS 由三個步驟組成,分別是“讀取-比較-寫回”。考慮這樣一種情況,線程1和線程2同時執行 CAS 邏輯,兩個線程的執行順序如下:

  • 時刻1:線程1執行讀取操作,獲取原值 A,然后線程被切換走

  • 時刻2:線程2執行完成 CAS 操作將原值由 A 修改為 B

  • 時刻3:線程2再次執行 CAS 操作,並將原值由 B 修改為 A

  • 時刻4:線程1恢復運行,將比較值(compareValue)與原值(oldValue)進行比較,發現兩個值相等。 

然后用新值(newValue)寫入內存中,完成 CAS 操作

  如上流程,線程1並不知道原值已經被修改過了,在它看來並沒什么變化,所以它會繼續往下執行流程。對於 ABA 問題,通常的處理措施是對每一次 CAS 操作設置版本號。

ABA問題的解決思路其實也很簡單,就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A→B→A就會變成1A→2B→3A了。

  從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標志是否等於預期標志,如果全部相等,則以原子方式將該引用和該標志的值設置為給定的更新值。

 2.循環時間長開銷大

  自旋CAS(不成功,就一直循環執行,直到成功) 如果長時間不成功,會給 CPU 帶來非常大的執行開銷。

3.只能保證一個共享變量的原子操作

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


四、CAS 與 Synchronized 的使用情景   

  1. 對於資源競爭較少(線程沖突較輕)的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態與內核態間的切換操作額外浪費消耗cpu資源;而CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。

  2. 對於資源競爭嚴重(線程沖突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。


免責聲明!

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



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