Java 基本功 之 CAS
本文首發於個人公眾號《andyqian》, 期待你的關注!
前言
在Java併發程式設計中,我們經常使用鎖對競爭資源予以併發控制,以解決資源競爭的問題。但無論是使用 Lock 還是 Synchronized,隨著鎖機制的引入,就不可避免的帶來另一個問題,也就鎖與解鎖時的上下文切換,執行緒等待 等效能問題。現在回過頭來看,在有些場景中,是否真的需要引入鎖才能解決競爭資源共享問題?答案是否定的,在JDK原始碼中,也為我們實現了。就是今天要介紹的另外一種無鎖方案-CAS,它大量應用於JUC 包中,也是atomic包中各類的底層原理,其重要行可想而知。
CAS 簡介
CAS 全稱為:Compare And Swap (比較與替換),其核心思想是:將記憶體值 Value 與期望值 A 進行比較,如果兩者相等,則將其設定為新值 B,否則不進行任何操作。CAS操作非常高效,在我看來,其原因有二,其一:底層呼叫的是 sun.misc.Unsafe 類,操作的是記憶體值,非常高效。其二:在多執行緒環境下,始終只有一個執行緒獲得執行權,未獲得執行權的執行緒並不會掛起而造成阻塞,而是以操作CAS失敗後再次執行CAS操作,直至成功,這一個過程,在Java中稱之為 “自旋”。
原始碼解析
Java 中 CAS 應用的十分廣泛,幕後英雄是sun.misc.Unsafe 類,單獨看Unsafe類的CAS操作可能有些茫然,以我們熟悉的 AtomicInteger 類中的 compareAndSet 方法為引子,再分析到 Unsafe類可能會更好些。
下面為AtomicInteger 類中compareAndSet 方法的原始碼,如下所述:
1 2 |
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update)} |
方法入參中 expect 為期望值, update 為待更新值。
繼續往下看,compareAndSet方法內部使用到的是Unsafe.compareAndSwapInt()方法,如下所述:
1 |
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); |
方法入參有四個,其中:
- Object var1 為物件。
- long var2 為 var1 物件的記憶體地址。
- int var4 為 記憶體地址 中的期望值。
- var5 為 待更新的值。
在Unsafe類中,同類的方法有以下幾個:
1 2 3 |
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); |
其實 Unsafe 類還給我們提供了一系列底層的API,由於篇幅原因,就不再展開說明,下次放單獨一篇文章中談談。
ABA 問題
在 CAS 中有一個特別經典的問題,也就是ABA。它說的是:記憶體值 Value 與期望值 A 進行比較前,Value已經發生過變化了,只不過是其變化後的值也為Value。從而造成從結果上看,其結果一致是一致的,(多發生於多執行緒條件下)當然這也是符合CAS 條件的。在大多數場景下,我們並不需要關心這種場景,在需要關心時,我們也可以使用JDK為我們提供了實現類 - AtomicStampedReference。在 AtomicStampedReference 類中,引入了標記位的概念,用於標記value值是否被修改過。結合value值 + 標記位是否一致,來判斷value值是否修改過。
其原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
public boolean compareAndSet(V expectedReference, // 期望引用物件 V newReference, // 新的引用物件 int expectedStamp, //期望標誌位 int newStamp) // 新的標識位 Pair<V> current = pair; // 獲取物件與標識引用對 return expectedReference == current.reference && // 期望物件引用是否等於當前引用物件 (是否發生變化) expectedStamp == current.stamp&& // 期望stamp 是否等於當前stamp ((newReference == current.reference && //新的引用物件是否等於當前引用物件,新的stamp是否等於當前stamp newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); //進行pair 的cas操作 |
其中 pair 為 物件引用與版本標記物件,其原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 |
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } |
結語
在Java 中 CAS 應用的十分廣泛,包括但不限於:Atomic,synchorized 底層原理等等。但需要明確的是 CAS 的存在並不是用來替換 Lock 的,而是一種互補的關係。平常都在寫業務程式碼,沒有更深層次的檢視原始碼,當檢視原始碼時,卻又是一件趣事,蠻好的!
相關閱讀:
1.《CORS跨域實踐》
2.《說說面試那些事》
3.《一個Java小細節!》
4.《記一個有趣的Java OOM!》
掃碼關注,一起進步
個人部落格: http://www.andyqian.c