引言
傳統的併發控制手段,如使用synchronized
關鍵字或者ReentrantLock
等互斥鎖機制,雖然能夠有效防止資源的競爭衝突,但也可能帶來額外的效能開銷,如上下文切換、鎖競爭導致的執行緒阻塞等。而此時就出現了一種樂觀鎖的策略,以其非阻塞、輕量級的特點,在某些場合下能更好地提升併發效能,其中最為關鍵的技術便是Compare And Swap(簡稱CAS)。
關於synchronize的實現原理,請看移步這篇文章:美團一面:說說synchronized的實現原理?問麻了。。。。
關於synchronize的鎖升級,請移步這篇文章:京東二面:Sychronized的鎖升級過程是怎樣的?
CAS是一種無鎖演算法,它在硬體級別提供了原子性的條件更新操作,允許執行緒在不加鎖的情況下實現對共享變數的修改。在Java中,CAS機制被廣泛應用於java.util.concurrent.atomic
包下的原子類以及高階併發工具類如AbstractQueuedSynchronizer
(AQS)的實現中。
CAS的基本概念與原理
CAS是一種原子指令,常用於多執行緒環境中的無鎖演算法。CAS操作包含三個基本運算元:記憶體位置、期望值和新值。在執行CAS操作時,計算機會檢查記憶體位置當前是否存放著期望值,如果是,則將記憶體位置的值更新為新值;若不是,則不做任何修改,保持原有值不變,並返回當前記憶體位置的實際值。
在Java中,CAS機制被封裝在jdk.internal.misc.Unsafe
類中,儘管這個類並不建議在普通應用程式中直接使用,但它是構建更高層次併發工具的基礎,例如java.util.concurrent.atomic
包下的原子類如AtomicInteger
、AtomicLong
等。這些原子類透過JNI呼叫底層硬體提供的CAS指令,從而在Java層面上實現了無鎖併發操作。
這裡指的注意的是,在JDK1.9之前CAS機制被封裝在
sun.misc.Unsafe
類中,在JDK1.9之後就使用了
jdk.internal.misc.Unsafe
。這點由java.util.concurrent.atomic
包下的原子類可以看出來。而sun.misc.Unsafe
被許多第三方庫所使用。
CAS實現原理
在Java中,雖然Java語言本身並未直接提供CAS這樣的原子指令,但是Java可以透過JNI
呼叫本地方法來利用硬體級別的原子指令實現CAS操作。在Java的標準庫中,特別是jdk.internal.misc.Unsafe
類提供了一系列compareAndSwapXXX
方法,這些方法底層確實是透過C++編寫的內聯彙編來呼叫對應CPU架構的cmpxchg
指令,從而實現原子性的比較和交換操作。
cmpxchg
指令是多數現代CPU支援的原子指令,它能在多執行緒環境下確保一次比較和交換操作的原子性,有效解決了多執行緒環境下資料競爭的問題,避免了資料不一致的情況。例如,在更新一個共享變數時,如果期望值與當前值相匹配,則原子性地更新為新值,否則不進行更新操作,這樣就能在無鎖的情況下實現對共享資源的安全訪問。
我們以java.util.concurrent.atomic
包下的AtomicInteger
為例,分析其compareAndSet
方法。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
//由這裡可以看出來,依賴jdk.internal.misc.Unsafe實現的
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public final boolean compareAndSet(int expectedValue, int newValue) {
// 呼叫 jdk.internal.misc.Unsafe的compareAndSetInt方法
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}
Unsafe
中的compareAndSetInt
使用了@HotSpotIntrinsicCandidate
註解修飾,@HotSpotIntrinsicCandidate
註解是Java HotSpot虛擬機器(JVM)的一個特性註解,它表明標註的方法有可能會被HotSpot JVM識別為“內聯候選”,當JVM發現有方法被標記為內聯候選時,會嘗試利用底層硬體提供的原子指令(比如cmpxchg
指令)直接替換掉原本的Java方法呼叫,從而在執行時獲得更好的效能。
public final class Unsafe {
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
}
compareAndSetInt
這個方法我們可以從openjdk
的hotspot
原始碼(位置:hotspot/src/share/vm/prims/unsafe.cpp
)中可以找到:
{CC "compareAndSetObject",CC "(" OBJ "J" OBJ "" OBJ ")Z", FN_PTR(Unsafe_CompareAndSetObject)},
{CC "compareAndSetInt", CC "(" OBJ "J""I""I"")Z", FN_PTR(Unsafe_CompareAndSetInt)},
{CC "compareAndSetLong", CC "(" OBJ "J""J""J"")Z", FN_PTR(Unsafe_CompareAndSetLong)},
{CC "compareAndExchangeObject", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeObject)},
{CC "compareAndExchangeInt", CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},
{CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},
關於openjdk的原始碼,本文原始碼版本為1.9,如需要該版本原始碼或者其他版本下載方法,請關注本公眾號【碼農Academy】後,後臺回覆【openjdk】獲取
而hostspot
中的Unsafe_CompareAndSetInt
函式會統一呼叫Atomic
的cmpxchg
函式:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
// 統一呼叫Atomic的cmpxchg函式
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END
而Atomic
的cmpxchg
函式原始碼(位置:hotspot/src/share/vm/runtime/atomic.hpp
)如下:
/**
*這是按位元組大小進行的`cmpxchg`操作的預設實現。它使用按整數大小進行的`cmpxchg`來模擬按位元組大小進行的`cmpxchg`。不同的平臺可以透過定義自己的內聯定義以及定義`VM_HAS_SPECIALIZED_CMPXCHG_BYTE`來覆蓋這個預設實現。這將導致使用特定於平臺的實現而不是預設實現。
* exchange_value:要交換的新值。
* dest:指向目標位元組的指標。
* compare_value:要比較的值。
* order:記憶體順序。
*/
inline jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest,
jbyte compare_value, cmpxchg_memory_order order) {
STATIC_ASSERT(sizeof(jbyte) == 1);
volatile jint* dest_int =
static_cast<volatile jint*>(align_ptr_down(dest, sizeof(jint)));
size_t offset = pointer_delta(dest, dest_int, 1);
// 獲取當前整數大小的值,並將其轉換為位元組陣列。
jint cur = *dest_int;
jbyte* cur_as_bytes = reinterpret_cast<jbyte*>(&cur);
// 設定當前整數中對應位元組的值為compare_value。這確保瞭如果初始的整數值不是我們要找的值,那麼第一次的cmpxchg操作會失敗。
cur_as_bytes[offset] = compare_value;
// 在迴圈中,不斷嘗試更新目標位元組的值。
do {
// new_val
jint new_value = cur;
// 複製當前整數值,並設定其中對應位元組的值為exchange_value。
reinterpret_cast<jbyte*>(&new_value)[offset] = exchange_value;
// 嘗試使用新的整數值替換目標整數。
jint res = cmpxchg(new_value, dest_int, cur, order);
if (res == cur) break; // 如果返回值與原始整數值相同,說明操作成功。
// 更新當前整數值為cmpxchg操作的結果。
cur = res;
// 如果目標位元組的值仍然是我們之前設定的值,那麼繼續迴圈並再次嘗試。
} while (cur_as_bytes[offset] == compare_value);
// 返回更新後的位元組值
return cur_as_bytes[offset];
}
而由cmpxchg
函式中的do...while
我們也可以看出,當多個執行緒同時嘗試更新同一記憶體位置,且它們的期望值相同但只有一個執行緒能夠成功更新時,其他執行緒的CAS操作會失敗。對於失敗的執行緒,常見的做法是採用自旋鎖的形式,即迴圈重試直到成功為止。這種方式在低競爭或短時間視窗內的併發更新時,相比於傳統的鎖機制,它避免了執行緒的阻塞和喚醒帶來的開銷,所以它的效能會更優。
Java中的CAS實現與API
在Java中,CAS操作的實現主要依賴於兩個關鍵元件:sun.misc.Unsafe
類、jdk.internal.misc.Unsafe
類以及java.util.concurrent.atomic
包下的原子類。儘管Unsafe
類提供了對底層硬體原子操作的直接訪問,但由於其API是非公開且不穩定的,所以在常規開發中並不推薦直接使用。Java標準庫提供了豐富的原子類,它們是基於Unsafe
封裝的安全、便捷的CAS操作實現。
java.util.concurrent.atomic
包
Java標準庫中的atomic
包為開發者提供了許多原子類,如AtomicInteger
、AtomicLong
、AtomicReference
等,它們均內建了CAS操作邏輯,使得我們可以在更高的抽象層級上進行無鎖併發程式設計。
原子類中常見的CAS操作API包括:
compareAndSet(expectedValue, newValue)
:嘗試將當前值與期望值進行比較,如果一致則將值更新為新值,返回是否更新成功的布林值。getAndAdd(delta)
:原子性地將當前值加上指定的delta值,並返回更新前的原始值。getAndSet(newValue)
:原子性地將當前值設定為新值,並返回更新前的原始值。
這些方法都是基於CAS原理,能夠在多執行緒環境下保證對變數的原子性修改,從而在不引入鎖的情況下實現高效的併發控制。
CAS的優缺點與適用場景
CAS摒棄了傳統的鎖機制,避免了因獲取和釋放鎖產生的上下文切換和執行緒阻塞,從而顯著提升了系統的併發效能。並且由於CAS操作是基於硬體層面的原子性保證,所以它不會出現死鎖問題,這對於複雜併發場景下的程式設計特別重要。另外,CAS策略下執行緒在無法成功更新變數時不需要掛起和喚醒,只需透過簡單的迴圈重試即可。
但是,在高併發條件下,頻繁的CAS操作可能導致大量的自旋重試,消耗大量的CPU資源。尤其是在競爭激烈的場景中,執行緒可能花費大量的時間在不斷地嘗試更新變數,而不是做有用的工作。這個由剛才cmpxchg
函式可以看出。對於這個問題,我們可以參考synchronize
中輕量級鎖經過自旋,超過一定閾值後升級為重量級鎖的原理,我們也可以給自旋設定一個次數,如果超過這個次數,就把執行緒掛起或者執行失敗。(自適應自旋)
另外,Java中的原子類也提供瞭解決辦法,比如LongAdder
以及DoubleAdder
等,LongAdder
過分散競爭點來減少自旋鎖的衝突。它並沒有像AtomicLong那樣維護一個單一的共享變數,而是維護了一個Base
值和一組Cell
(桶)結構。每個Cell
本質上也是一個可以進行原子操作的計數器,多個執行緒可以分別在一個獨立的Cell
上進行累加,只有在必要時才將各個Cell
的值彙總到Base
中。這樣一來,大部分時候執行緒間的修改不再是集中在同一個變數上,從而降低了競爭強度,提高了併發效能。
- ABA問題:
單純的CAS無法識別一個值被多次修改後又恢復原值的情況,可能導致錯誤的判斷。比如現在有三個執行緒:
即執行緒1將str從A改成了B,然後執行緒3將str又從B改成了A,而此時對於執行緒2來說,他就覺得這個值還是A,所以就不會在更改了。
而對於這個問題,其實也很好解決,我們給這個資料加上一個時間戳或者版本號(樂觀鎖概念)。即每次不僅比較值,還會比較版本。比如上述示例,初始時str的值的版本是1,然後執行緒2操作後值變成B,而對應版本變成了2,然後執行緒3操作後值變成了A,版本變成了3,而對於執行緒2來說,雖然值還是A,但是版本號變了,所以執行緒2依然會執行替換的操作。
Java的原子類就提供了類似的實現,如AtomicStampedReference
和AtomicMarkableReference
引入了附加的標記位或版本號,以便區分不同的修改序列。
總結
Java中的CAS原理及其在併發程式設計中的應用是一項非常重要的技術。CAS利用CPU硬體提供的原子指令,實現了在無鎖環境下的高效併發控制,避免了傳統鎖機制帶來的上下文切換和執行緒阻塞開銷。Java透過JNI介面呼叫底層的CAS指令,封裝在jdk.internal.misc
類和java.util.concurrent.atomic
包下的原子類中,為我們提供了簡潔易用的API來實現無鎖程式設計。
CAS在帶來併發效能提升的同時,也可能引發迴圈開銷過大、ABA問題等問題。針對這些問題,Java提供瞭如LongAdder
、AtomicStampedReference
和AtomicMarkableReference
等工具類來解決ABA問題,同時也透過自適應自旋、適時放棄自旋轉而進入阻塞等待等方式降低迴圈開銷。
理解和熟練掌握CAS原理及其在Java中的應用,有助於我們在開發高效能併發程式時作出更明智的選擇,既能提高系統併發效能,又能保證資料的正確性和一致性。
本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等