CAS樂觀鎖(原子操作)


鎖主要分為兩種:樂觀鎖和悲觀鎖,而 synchronized 就屬於一種悲觀鎖,每次在操作數據前都會加鎖。樂觀鎖是指:樂觀的認為自己在操作數據時,別人不會對當前數據進行修改,因此不會加鎖。如果有人對數據進行了修改,則重新獲取修改后的數據,進行操作。直到成功為止。而樂觀鎖的這種機制就是CAS(compare and swap)比較並交換。

一、什么是 CAS


CAS(Compare And Swap | Compare And Set)比較並交換,CAS 是解決多線程並行情況下使用鎖造成性能消耗的一種機制。CAS 操作包含三個操作數:內存位置(V)、預期值(A)、新值(B)。如果內存位置的值(V)與逾期原值(A)相同,處理器會將該位置的值更新為新值(B)則 CAS 操作成功。否則,處理器不做任何更改,只需要將當前位置的值進行返回即可。在 Java 可以通過鎖和循環 CAS 的方式來實現原子操作。Java 中 java.util.concurrent.atomic 包相關類就是 CAS 的實現。我們就舉一個整數的例子:
【1】代碼解析:AtomicInteger:通常情況下,在 Java中,i++ 等類似操作並不是線程安全的,因為 i++ 可分為三個獨立的操作:獲取變量當前值,為該值+1,然后寫回新的值。在多線程的情況下 +1000 得到的值往往是不正確的。即使變量被 volatile 修飾,但可以用原子方式 AtomicInteger 自增,這樣可以保證數據的原子性。代碼如下:

建議:必須具備 volatile 的基本知識,因為 AtomicInteger是 volatile 不具備原子性的解決方案之一。

  getAndIncrement 方法:如果當前值 == 預期值,則以原子方式將該值設置為給定的更新值。

1 public class CAS {
2     public static void main(String[] args) {
3         //創建一個原子整數,當前值為默認值0
4     AtomicInteger atomicInteger = new AtomicInteger();
5         //調用 CAS 方法進行自增
6     atomicInteger.getAndIncrement();
7     }
8 }

【2】進入 getAndIncrement() 方法,發現底層調用的是 Unsafe 類的 getAndIncrement 方法

Unsafe 是 CAS 的核心類,由於 Java 方法無法直接訪問底層系統,需要通過本地方法(native)方法來訪問。Unsafe 相當於一個后門,基於該類可以直接操作特定內存的數據。Unsafe 類存在於 sun.misc 包中,其內部方法操作可以像 C的指針一樣直接操作內存,因為 Java 中 CAS 的操作依賴於 Unsafe 類的方法。注意 Unsafe 類中的所有方法都是 native 修飾的,也就是說 Unsafe 類中的方法都直接調用操作系統底層資源執行相應任務。這種操作時不可分割的,具有原子性。

  valueOffset 參數:表示變量值在內存中的偏移地址,因為 Unsafe 就是根據內存偏移地址獲取數據的。

1 public final int getAndIncrement() {
2     return unsafe.getAndAddInt(this, valueOffset, 1);
3 }

CAS 是一條 CPU 並發原語(原語屬於操作系統范疇,是由若干指令組成,用於完成某個功能的一個過程,並且原語執行必須是連續的,在執行過程中不允許被中斷,也就是說 CAS 是一條 CPU 的原子指令,不會造成所謂的數據不一致問題)體現在 Java 語言中就是 sun.misc.Unsafe 類中的各個方法。調用 Unsafe 類中的 CAS方法,JVM 會幫我們編譯出 CAS匯編指令。這是一中完全依賴於硬件的功能,通過它實現原子操作。

 【3】進入Unsafe 類的 getAndAddInt 方法:我們發現其通過無限循環去解決鎖的問題,也稱為 “循環鎖”,直到修改成功。代碼及注釋說明如下:

1 public final int getAndAddInt(Object var1, long var2, int var4){
2     int var5;
3     do{
4         //根據對象和地址偏移量獲取內存中的值
5         var5 = this.getIntVolatile(var1, var2);
6     //將獲取到的值 var5 傳入,此方法內部會先比較var2地址的值是否等於 var5,相等則修改var5值並返回,否則重新進入循環。
7     }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
8         return var5;
9 }

二、CAS 缺點


【1】循環時間長開銷很大:自旋 CAS 如果長時間不成功,會給 CPU 帶來非常大的執行開銷。如果 JVM 能支持處理器提供的 pause 指令,那么效率會有一定的提升,pause 指令有兩個作用:第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序沖突(memory order violation)而引起 CPU 流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
【2】只能保證一個共享變量的原子操作:只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環 CAS 的方式來保證原子操作,但是對多個共享變量操作時,循環 CAS 就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合並成一個共享變量來操作。比如有兩個共享變量 i=2,j=a,合並一下 ij=2a,然后用CAS 來操作 ij。從 Java1.5 開始 JDK 提供了 AtomicReference<Clazz> 類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行 CAS 操作。
【3】ABA 問題:因為 CAS 需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用 CAS 進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA 問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

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

三、實戰應用


Netty 中的 ByteBuf 的內存回收使用了一種引用計數法的算法,判斷當前對象的引用是否為零,如果為零則對對象進行回收。在引用計數的加法的操作,使用到了CAS,代碼實例如下:

 1 public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
 2     //管理 AbstractReferenceCountedByteBuf 對象中的 refCnt 屬性
 3     private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater
 4        = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
 5     private volatile int refCnt = 1;
 6     private ByteBuf retain0(int increment) {
 7         int refCnt;
 8         int nextCnt;
 9         do {
10             refCnt = this.refCnt;
11             nextCnt = refCnt + increment;
12             if(nextCnt <= increment) {
13                 throw new IllegalReferenceCountException(refCnt, increment);
14             }
15         } while(!refCntUpdater.compareAndSet(this, refCnt, nextCnt));
16 
17         return this;
18     }
19 }
 


免責聲明!

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



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