CAS與ABA問題及解決方式
要了解ABA問題,我們得先知道什么是CAS,CAS 全稱是 compare and swap,是一種用于在多線程環(huán)境下實現(xiàn)同步功能的機(jī)制。CAS的出現(xiàn)主要是為了解決多線程并發(fā)情況下,數(shù)據(jù)的不一致問題。
CAS底層原理
CAS 的思想很簡單:三個參數(shù),一個當(dāng)前內(nèi)存值 V、舊的預(yù)期值 A、即將更新的值 B,當(dāng)且僅當(dāng)預(yù)期值 A 和內(nèi)存值 V 相同時,將內(nèi)存值修改為 B 并返回 true,否則什么都不做,并返回 false
Unsafe類
Unsafe類是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地(native)方法來訪問,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。Unsafe類存在與sum.misc包中,其內(nèi)部實現(xiàn)是C++寫的,我從JDK1.8源碼中截取了關(guān)鍵代碼
- UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
- UnsafeWrapper("Unsafe_CompareAndSwapInt");
- oop p = JNIHandles::resolve(obj);
- jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
- return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
- UNSAFE_END
從上面代碼可以看出最后調(diào)用的是Atomic:comxchg這個方法,這個方法的實現(xiàn)放在hotspot下的os_cpu包中,說明這個方法的實現(xiàn)和操作系統(tǒng)、CPU都有關(guān)系,以多核CPU為例:
- 首先會判斷CPU是否為多核,如果是多核加一個lock內(nèi)存屏障,這樣就可以防止多線程并發(fā)情況競爭發(fā)生
- 進(jìn)行對比交換,調(diào)用匯編指令cmpxchg獲取新值并設(shè)值。
CAS問題
cas實現(xiàn)
從JDK1.5開始,java.util.concurrent包為我們提供了許多cas操作類諸如:AtomicInteger,
AtomicLong,AtomicReference

上圖運行過程中可能會出現(xiàn)兩個問題:
- 線程3可能一直拿不到最新的值,導(dǎo)致線程自旋
- 主內(nèi)存有個數(shù)據(jù)值:A,兩個線程A和B分別copy主內(nèi)存數(shù)據(jù)到自己的工作區(qū),A執(zhí)行比較慢,需要10秒, B執(zhí)行比較快,需要2秒, 此時B線程將主內(nèi)存中的數(shù)據(jù)更改為B,過了一會又更改為A,然后A線程執(zhí)行比較,發(fā)現(xiàn)結(jié)果是A,以為別人沒有動過,然后執(zhí)行更改操作。其實中間已經(jīng)被更改過了,這就是ABA問題。
ABA問題的優(yōu)化
ABA問題導(dǎo)致的原因,是CAS過程中只簡單進(jìn)行了“值”的校驗,再有些情況下,“值”相同不會引入錯誤的業(yè)務(wù)邏輯(例如庫存),有些情況下,“值”雖然相同,卻已經(jīng)不是原來的數(shù)據(jù)了。那如何能避免ABA問題呢?優(yōu)化的方式也很簡單,就是不能只對值進(jìn)行比較,通過對值打標(biāo)簽的方式就能很好的避免ABA問題。JAVA中也為我們提供了相應(yīng)的處理類AtomicStampReferenceAtomicStampReference在cas的基礎(chǔ)上增加了一個標(biāo)記stamp,使用這個標(biāo)記可以用來覺察數(shù)據(jù)是否發(fā)生變化,給數(shù)據(jù)帶上了一種實效性的檢驗。它有以下幾個參數(shù):
- //參數(shù)代表的含義分別是 期望值,寫入的新值,期望標(biāo)記,新標(biāo)記值
- public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
- public V getRerference();
- public int getStamp();
- public void set(V newReference,int newStamp);
我們通過一個示例來說明:
- public class Test {
- private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
- public static void main(String[] args) {
- new Thread(() -> {
- atomicReference.compareAndSet(100, 101);
- atomicReference.compareAndSet(101, 100);
- },"t1").start();
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(atomicReference.compareAndSet(100, 2021) + "\t修改后的值:" + atomicReference.get());
- },"t2").start();
- }
- }
- 初始值為100,線程t1將100改成101,然后又將101改回100
- 線程t2先睡眠1秒,等待t1操作完成,然后t2線程將值改成2019
可以看到,線程2修改成功。輸出結(jié)果:
- true 修改后的值:2021
要解決ABA問題,可以增加一個版本號,當(dāng)內(nèi)存位置V的值每次被修改后,版本號都加1AtomicStampedReference內(nèi)部維護(hù)了對象值和版本號,在創(chuàng)建AtomicStampedReference對象時,需要傳入初始值和初始版本號, 當(dāng)AtomicStampedReference設(shè)置對象值時,對象值以及狀態(tài)戳都必須滿足期望值,寫入才會成功
- public class Test {
- private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
- public static void main(String[] args) {
- new Thread(() -> {
- System.out.println("t1拿到的初始版本號:" + atomicStampedReference.getStamp());
- //睡眠1秒,是為了讓t2線程也拿到同樣的初始版本號
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
- atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
- },"t1").start();
- new Thread(() -> {
- int stamp = atomicStampedReference.getStamp();
- System.out.println("t2拿到的初始版本號:" + stamp);
- //睡眠3秒,是為了讓t1線程完成ABA操作
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("最新版本號:" + atomicStampedReference.getStamp());
- System.out.println(atomicStampedReference.compareAndSet(100, 2021,stamp,atomicStampedReference.getStamp() + 1) + "\t當(dāng)前 值:" + atomicStampedReference.getReference());
- },"t2").start();
- }
- }
- 初始值100,初始版本號1
- 線程t1和t2拿到一樣的初始版本號
- 線程t1完成ABA操作,版本號遞增到3
- 線程t2完成CAS操作,最新版本號已經(jīng)變成3,跟線程t2之前拿到的版本號1不相等,操作失敗
輸出結(jié)果:
- t1拿到的初始版本號:1
- t2拿到的初始版本號:1
- 最新版本號:3
- false當(dāng)前 值:100
【編輯推薦】