CAS與ABA問題的解決

聲明:本文爲作者原創,如若轉發,請指明轉發地址

1、CAS是什麼?

interface Account {
    // 獲取餘額
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法內會啓動 1000 個線程,每個線程做 -10 元 的操作
     * 如果初始餘額爲 10000 那麼正確的結果應當是 0
     */
    static void demo(Account account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
                + " cost: " + (end - start) / 1000_000 + " ms");
    }
}

//下面的方法時線程不安全的
class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        return this.balance;
    }

    @Override
    public void withdraw(Integer amount) {
        this.balance -= amount;
    }
}

//測試類
public class TestAccount {
    public static void main(String[] args) {
        Account account = new AccountUnsafe(10000);
        Account.demo(account);
    }
}

1、解決方法:使用synchronized解決線程安全問題

//對成員變量使用同步保證線程安全
class AccountUnsafe implements Account {

    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this) {
            return this.balance;
        }
    }

    @Override
    public void withdraw(Integer amount) {
        synchronized (this) {
            this.balance -= amount;
        }
    }
}

2、解決方法:使用無鎖CAS解決線程安全問題

//使用無鎖的方式也能保證線程安全
class AccountCas implements Account {

    private AtomicInteger balance;

    public AccountCas(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 獲取餘額的最新值
            int prev = balance.get();
            // 修改餘額
            int next = prev - amount;
            // 真正修改
            /*
                compareAndSet 正是做這個檢查,在 set 前,先比較 prev 與當前值
                - 不一致了,next 作廢,返回 false 表示失敗
                比如,別的線程已經做了減法,當前值已經被減成了 990
                那麼本線程的這次 990 就作廢了,進入 while 下次循環重試
                - 一致,以 next 設置爲新值,返回 true 表示成功
            */
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

其中的關鍵是 compareAndSet,它的簡稱就是 CAS (也有 Compare And Swap 的說法),它必須是原子操作。

在這裏插入圖片描述

CAS有三個操作數,舊值prev,主存中的新值,要更改成的新值next。當且僅當舊值prev和主存中的新值一致時,纔會將主存中的值更改爲新值next,否則什麼也不做。

上面的流程圖:線程1和線程2同時更新同一變量Account對象的值

在這裏插入圖片描述

(1) 線程1和線程2從主存中讀取Account=100到自己的工作內存中

(2) 線程1將Account=100修改爲90,並將結果同步到主存中(寫屏障的原因),因此此時主存中Account=90

(3) 線程2向要通過CAS操作將Account變量的值修改爲90,於是在set之前就會重新讀取主存的值(讀屏障的原因)並與工作內存中的值進行比較,如果相同就說明沒有其他線程更改共享數據,成功的將主存中的值修改爲90,但是如果不一致就說明CAS失敗,此時就會進行下一次CAS操作(CAS自旋,前提在while循環內)

具體執行流程圖:

在這裏插入圖片描述

2、CAS需要volatile的支持

獲取共享變量時,爲了保證該變量的可見性,需要使用 volatile 修飾。它可以用來修飾成員變量和靜態成員變量,他可以避免線程從自己的工作緩存中查找變量的值,必須到主存中獲取它的值,線程操作 volatile 變量都是直接操作主存。即一個線程對 volatile 變量的修改,對另一個線程可見。

注意:

volatile 僅僅保證了共享變量的可見性,讓其它線程能夠看到最新值,但不能解決指令交錯問題(不能保證原
子性)

CAS 必須藉助 volatile 才能讀取到共享變量的最新值來實現【比較並交換】的效果

public class AtomicInteger extends Number implements java.io.Serializable {
    private volatile int value;
  
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
}

3、爲什麼CAS+volatile效率更高?

無鎖情況下,即使重試失敗,線程始終在高速運行,沒有停歇,而 synchronized 會讓線程在沒有獲得鎖的時
候,發生上下文切換,進入阻塞。

打個比喻:
線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車要減速、熄火,
等被喚醒又得重新打火、啓動、加速… 恢復到高速運行,代價比較大
但無鎖情況下,因爲線程要保持運行,需要額外 CPU 的支持,CPU 在這裏就好比高速跑道,沒有額外的跑
道,線程想高速運行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,仍然會進入可運行狀態,還
是會導致上下文切換。

結合 CAS + volatile 可以實現無鎖併發,適用於線程數少、多核 CPU 的場景下。

CAS 是基於樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關係,我喫虧點再
重試唄。
synchronized 是基於悲觀鎖的思想:最悲觀的估計,得防着其它線程來修改共享變量,我上了鎖你們都別想
改,我改完了解開鎖,你們纔有機會。

CAS 體現的是無鎖併發、無阻塞併發,請仔細體會這兩句話的意思。因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一,但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。

CAS缺點:

  1. CPU開銷較大,多線程反覆嘗試更新某一個變量的時候容易出現;
  2. 不能保證代碼塊的原子性,只能保證變量的原子性操作;
  3. ABA問題

4、AtomicInteger?

注意:如果是這個問題,就是在問你CAS,你只需要將CAS講一遍,然後再加上本類的特點即可。

class AccountCas implements Account {
    private AtomicInteger balance;
    
    public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
    
    @Override
    public Integer getBalance() {return balance.get();}

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            int prev = balance.get();
            int next = prev - amount;
            // 真正修改(因爲該變量balance是AtomicInteger類型的,因此可以調用該方法)
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

除了AtomicInteger類中的CAS方法外,還有其他封住的一些比較方便的方法(CAS需要放在while循環中):

AtomicInteger i = new AtomicInteger(0);

// 獲取並自增(i = 0, 結果 i = 1, 返回 0),類似於 i++
System.out.println(i.getAndIncrement());
// 自增並獲取(i = 1, 結果 i = 2, 返回 2),類似於 ++i
System.out.println(i.incrementAndGet());

// 自減並獲取(i = 2, 結果 i = 1, 返回 1),類似於 --i
System.out.println(i.decrementAndGet());
// 獲取並自減(i = 1, 結果 i = 0, 返回 1),類似於 i--
System.out.println(i.getAndDecrement());

// 獲取並加值(i = 0, 結果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值並獲取(i = 5, 結果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));

// 獲取並更新(i = 0, p 爲 i 的當前值, 結果 i = -2, 返回 0)
// 其中函數中的操作能保證原子,但函數需要無副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新並獲取(i = -2, p 爲 i 的當前值, 結果 i = 0, 返回 0)
// 其中函數中的操作能保證原子,但函數需要無副作用
System.out.println(i.updateAndGet(p -> p + 2));

將上面的CAS操作改成下面的方法會簡便很多:不需要使用while循環,並且簡化了操作步驟

class AccountCas implements Account {
    private AtomicInteger balance;
    
    public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
    
    @Override
    public Integer getBalance() {return balance.get();}

    @Override
    public void withdraw(Integer amount) {
       balance.getAndAdd(-1*amount)
    }
}

5、CAS和ABA的問題如何解決?

AtomicReference(原子引用)
AtomicMarkableReference
AtomicStampedReference

class DecimalAccountCas implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    //獲取餘額
    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while(true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

1、什麼是ABA問題?

因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了

@Slf4j(topic = "c.TestCAS")
public class TestCAS {
    static AtomicReference<String> ref = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        //獲取共享變量的舊值A
        String prev = ref.get();
		//調用other()方法
        other();

        Thread.sleep(1000);
        
        // CAS操作將A改爲C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    //其他線程將共享變量從A改成B,又從B改成A,但主線程感知不到,主線程只會判斷最新獲取到的值A與prev是否相同
    private static void other() throws InterruptedException {
        new Thread(() -> {
            //線程t1將A改成B
            log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();

        Thread.sleep(500);

        new Thread(() -> {
            //線程t2將B改成A
            log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

執行結果:

14:05:10.388 c.TestCAS [main] - main start...
14:05:10.544 c.TestCAS [t1] - change A->B true
14:05:11.049 c.TestCAS [t2] - change B->A true
14:05:12.047 c.TestCAS [main] - change A->C true

在這裏插入圖片描述

如圖所示:

(1) t1線程讀取主存中的Ref引用變量的值A到自己的工作內存中,將其從A改爲了B,並同步到了主存

(2) t2線程讀取主存中的Ref引用變量的值B到自己的工作內存中,將其從B改成了A,並同步到了主存

(3) 此時主存中的值經歷了A----》B---》A的過程,但是主線程感知不到

(4) 主線程進行CAS操作將Ref變量的值更改爲了C(將工作線程中的值和主存中的值進行比較,發現一致,就認爲主存中的共享變量的值沒有更改過)

主線程僅能判斷出共享變量的值與最初值 A 是否相同,不能感知到這種從 A 改爲 B 又 改回 A 的情況,如果主線程
希望:只要有其它線程【動過了】共享變量,那麼自己的 cas 就算失敗,這時,僅比較值是不夠的,需要再加一個版本號 。

2、AtomicStampedReference

在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

@Slf4j(topic = "c.TestCAS1")
public class TestCAS1 {
    //原子引用變量的初始值爲A
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        //獲取舊值A
        String prev = ref.getReference();
        // 獲取版本號
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);

        // 如果中間有其它線程干擾,發生了 ABA 現象
        other();

        Thread.sleep(1000);

        // 主線程嘗試改爲 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    private static void other() throws InterruptedException {
        //每次更新成功後,將版本號加1
        new Thread(() -> {
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本爲 {}", ref.getStamp());
        }, "t1").start();

        Thread.sleep(500);

        //每次更新成功後,將版本號加1
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本爲 {}", ref.getStamp());
        }, "t2").start();
    }
}

執行結果:

14:04:28.851 c.TestCAS1 [main] - main start...
14:04:28.856 c.TestCAS1 [main] - 版本 0
14:04:28.977 c.TestCAS1 [t1] - change A->B true
14:04:28.977 c.TestCAS1 [t1] - 更新版本爲 1
14:04:29.482 c.TestCAS1 [t2] - change B->A true
14:04:29.483 c.TestCAS1 [t2] - 更新版本爲 2
14:04:30.477 c.TestCAS1 [main] - change A->C false

在這裏插入圖片描述

(1) 線程1從主存中將Ref引用變量的值和Stamp版本號的值讀入到工作內存中,並將Ref從A改成了B,Stamp從0改成了1

(2) 線程1從主存中將Ref引用變量的值和Stamp版本號的值讀入到工作內存中,並將Ref從B改成了A,Stamp從1改成了2

(3) 此時主存中的Ref引用變量的值爲A,Stamp版本號的值爲2

(4) 主線程在進行CAS操作時,會將主存中的Ref變量的值A(新值)和工作內存中Ref變量的值A(舊值)進行比較,判斷是否相同(相同),同時還會將主存中Stamp版本號的值2(新值)和工作內存中Stamp版本號的值0(舊值)進行比較,判斷是否相同(不同),只有兩者都相同,CAS纔會成功。

AtomicStampedReference 可以給原子引用加上版本號,追蹤原子引用整個的變化過程,如: A -> B -> A ->
C ,通過AtomicStampedReference,我們可以知道,引用變量中途被更改了幾次。

但是有時候,並不關心引用變量更改了幾次,只是單純的關心是否更改過

3、AtomicMarkableReference

AtomicMarkableReference可以理解爲上面AtomicStampedReference的簡化版,就是不關心修改過幾次,僅僅關心是否修改過。因此變量mark是boolean類型,僅記錄值是否有過修改。

@Slf4j(topic = "c.Test38")
public class Test38 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("裝滿了垃圾");
        // 參數2 mark 可以看作一個標記,表示垃圾袋滿了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.debug("start...");
        //獲取垃圾袋
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        new Thread(() -> {
            log.debug("start...");
            bag.setDesc("空垃圾袋");
            //保潔阿姨將垃圾桶的垃圾倒空,並將垃圾狀態從true(滿)改爲false(空)
            ref.compareAndSet(bag, bag, true, false);
            log.debug(bag.toString());
        },"保潔阿姨").start();

        sleep(1);
        log.debug("想換一隻新垃圾袋?");
        //房東想要更換垃圾袋,發現垃等換失敗,因爲垃圾袋狀態爲false(空),但是期待的爲true(滿)
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("換了麼?" + success);
        log.debug(ref.getReference().toString());
    }
}

class GarbageBag {
    String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}

執行結果:

14:43:55.104 c.Test38 [main] - start...
14:43:55.113 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 裝滿了垃圾
14:43:55.254 c.Test38 [保潔阿姨] - start...
14:43:55.254 c.Test38 [保潔阿姨] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
14:43:56.277 c.Test38 [main] - 想換一隻新垃圾袋?
14:43:56.277 c.Test38 [main] - 換了麼?false
14:43:56.277 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋

在這裏插入圖片描述

(1) 保潔阿姨線程從主存中讀取bag變量到工作內存中,並將當前垃圾袋倒空,但是並沒有更換垃圾袋同時將flag的狀態從true更改爲false然後同步到主存中。

(2) 此時主存中的bag還是原來的bag(還是原來的垃圾袋),標記變量boolean flag = false

(3) 此時房東線程想要通過CAS更換垃圾袋,首先將工作內存中的垃圾袋bag和主存中的垃圾袋bag進行比較,判斷是否相同(相同),然後將工作內存中的flag=true與主存中的flag=false進行比較,判斷是否相同(不同),自由兩個都相同,CAS才能成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章