CAS致使的ABA問題及解決

Java併發--非阻塞同步

CAS問題引入

在併發問題中,最早想到的無疑是互斥同步,但線程阻塞和喚醒帶來了很大的性能問題,同步鎖的核心無非是防止共享變量併發修改帶來的問題,但不是任什麼時候候都有這樣的競爭關係。java

什麼是CAS

CAS,比較並交換(Compare-and-Swap,CAS),若是指望值和主內存值同樣,則交換要更新的值,也稱樂觀鎖。數組

如線程甲從主內存中拷貝了變量A爲1,在本身的線程中將副本A改成了10,當線程甲準備把這個變量更新到主內存時,若是主內存A的值不改變(指望值),仍是1,那麼線程甲成功更新主內存中A的值。但若是主內存A的值已經先被其餘線程改掉不爲1,那麼線程甲不斷地重試,直到成功爲止(自旋)。併發

CAS來自哪

CAS屬於J.U.C包,調用的Unsafe 類中方法,這是一種硬件支持的原子性操做,不能被打斷或中止,無需互斥同步。性能

以AtomicInteger下的getAndAddInt方法爲例,U即Unsafe類。this

/** * @param expectedValue 指望值 * @param newValue 新值 * @return 比價更新是否成功. */
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}
複製代碼

再往下看,經過 getIntVolatile(o, offset) 獲得之前的值v,經過調用 weakCompareAndSetInt() 來進行 CAS 比較,若是該字段內存地址中的值等於 v,那麼就更新內存地址爲 o+offset的變量爲 v + delta。getAndAddInt()方法 在一個循環中進行,發生衝突的作法是不斷的進行重試atom

/** * @param o 更新字段/元素的對象/數組 * @param offset 字段/元素偏移量 * @param delta 要添加的值,步長 * @return 之前的值 * @since 1.8 */
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}
複製代碼

運行代碼示例

線程t1,t2,同時修改主內存的一變量值,人爲的讓B快與Aspa

public class TestCAS {
    // 主內存atomicInteger初始值爲1
    public static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        // A線程計劃將值改成10,先休眠2s,再比較交換
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("當前線程: "+Thread.currentThread().getName()+"比較交換結果:"
                    +atomicInteger.compareAndSet(1, 10)+" 如今值爲:"+atomicInteger.get());
        },"t1").start();

        // B線程計劃將值改成10,不休眠
        new Thread(() -> {
            System.out.println("當前線程: "+Thread.currentThread().getName()+"比較交換結果:"
                    +atomicInteger.compareAndSet(1, 20)+" 如今值爲:"+atomicInteger.get());
        },"t2").start();
    }
}
複製代碼

控制檯線程

當前線程: t2比較交換結果:true 如今值爲:20
當前線程: t1比較交換結果:false 如今值爲:20
複製代碼

這樣不用加同步鎖,就實現了變量的併發修改帶來的問題。3d

若是你的好朋友向你借走了10塊,次日他又還給你了10塊,若是的你的朋友只是爲了買包零食,你可能不會在意,如何他用那10塊中了大獎,你可能會有些着急了...code

ABA問題引入

上個代碼中,存在一個問題。如:t1,t2線程都拷貝到變量atomicInteger=1,若是B線程優先級較高或運氣好,第一次,t2先將atomicInteger修改成20併成功寫入主內存,接着t2又拷貝到atomicInteger=20,將副本又改成1,併成功寫回主內存。第三次,t1拿到主內存atomicInteger的值。可這個值已經被t2修改過兩次,會有問題嗎?

ABA問題

若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。

ABA解決

  1. 互斥同步鎖synchronized

  2. 若是項目只在意數值是否正確, 那麼ABA 問題不會影響程序併發的正確性。

  3. J.U.C 包提供了一個帶有時間戳的原子引用類 AtomicStampedReference 來解決該問題,它經過控制變量的版本來保證 CAS 的正確性。

AtomicStampedReference代碼示例

public class SolveCAS {
    // 主內存共享變量,初始值爲1,版本號爲1
    private static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);


    public static void main(String[] args) {
        // t1,指望將1改成10
        new Thread(() -> {
            // 第一次拿到的時間戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次時間戳:"+stamp+" 值爲:"+atomicStampedReference.getReference());
            // 休眠5s,確保t2執行完ABA操做
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            // t2將時間戳改成了3,cas失敗
            boolean b = atomicStampedReference.compareAndSet(1, 10, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" CAS是否成功:"+b);
            System.out.println(Thread.currentThread().getName()+" 當前最新時間戳:"+atomicStampedReference.getStamp()+" 最新值爲:"+atomicStampedReference.getReference());
        },"t1").start();

        // t2進行ABA操做
        new Thread(() -> {
            // 第一次拿到的時間戳
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+" 第1次時間戳:"+stamp+" 值爲:"+atomicStampedReference.getReference());
            // 休眠,修改前確保t1也拿到一樣的副本,初始值爲1
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            // 將副本改成20,再寫入,緊接着又改成1,寫入,每次提高一個時間戳,中間t1沒介入
            atomicStampedReference.compareAndSet(1, 20, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+" 第2次時間戳:"+atomicStampedReference.getStamp()+" 值爲:"+atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(20, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName()+" 第3次時間戳:"+atomicStampedReference.getStamp()+" 值爲:"+atomicStampedReference.getReference());

        },"t2").start();
    }
}
複製代碼

控制檯

t1 第1次時間戳:1 值爲:1
t2 第1次時間戳:1 值爲:1
t2 第2次時間戳:2 值爲:20
t2 第3次時間戳:3 值爲:1
t1 CAS是否成功:false
t1 當前最新時間戳:3 最新值爲:1
複製代碼
相關文章
相關標籤/搜索