CAS詳解及ABA問題的解決

序言

由於最近項目上遇到了高併發問題,而自己對高併發,多線程這裏的知識點相對薄弱,尤其是基礎,所以想系統的學習一下,以後可能會出一系列的JUC文章及總結 ,同時也爲企業級的高併發項目做好準備。

本文是JUC文章的第二篇,如想看以往關於JUC文章,請點擊JUC系列總結

此係列文章的總結思路大致分爲三部分:

  1. 理論(概念);
  2. 實踐(代碼證明);
  3. 總結(心得及適用場景)。

在這裏提前說也是爲了防止大家看着看着就迷路了。

CAS大綱

首先,下圖是本文的大綱,也就是說在看本文之前,你需要先了解本文到底是講什麼內容,有個整體大觀,然後逐個細分到內容層次去講解。

CAS.png

CAS理論

在上一文中(從代碼實踐的角度解析volatile關鍵字),我們介紹了i++的爲什麼不保證原子性以及i++的一些處理辦法,其中有一種方式是採用AtomicInteger類,但是你知道爲什麼麼?

查看AtomicInteger.getAndIncrement()方法,發現其沒有加synchronized也實現了同步。這是爲什麼?

什麼是CAS?

CAS的全稱是Compare-And-Swap,它是一條CPU併發原語。

正如它的名字一樣,比較並交換,它是一種很重要的同步思想。如果主內存的值跟期望值一樣,那麼就進行修改,否則一直重試,直到一致爲止。

而原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致性問題。

它的功能是判斷內存某個位置的值是否爲預期值,如果是則更改爲新的值,這個過程是原子的

我們來看一段代碼:

public class CasDemo {
    public static void main(String[] args) {
        //初始值
        AtomicInteger integer = new AtomicInteger(5);
        //比較並替換
        boolean flag = integer.compareAndSet(5, 10);
        boolean flag2 = integer.compareAndSet(5, 15);

        System.out.println("是否自選並替換 \t"+flag +"\t更改之後的值爲:"+integer.get());
        System.out.println("是否自選並替換 \t"+flag2 +"\t更改之後的值爲:"+integer.get());
    }
}

你能猜到答案麼?

是否自選並替換 	true 	更改之後的值爲:10
是否自選並替換 	false	更改之後的值爲:10

第一次修改,期望值爲5,主內存也爲5,修改成功,爲10。

第二次修改,期望值爲5,主內存爲10,修改失敗。

CAS原理

在翻了源碼之後,大致可以總結出兩個關鍵點:

  1. 自旋;
  2. unsafe類。

當點開compareAndSet方法後:

// AtomicInteger類內部
public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

通過這個方法,我們可以找出AtomicInteger內部維護了volatile int valueprivate static final Unsafe unsafe兩個比較重要的參數。(注意value是用volatile修飾

變量valueOffset,表示該變量在內存中的偏移地址,因爲Unsafe就是根據內存偏移地址獲取數據的。

變量value用volatile修飾,保證了多線程之間的內存可見性

// AtomicInteger類內部
private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

然後我們通過compareAndSwapInt找到了unsafe類核心方法:

//unsafe內部類
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

AtomicInteger.compareAndSwapInt()調用了Unsafe.compareAndSwapInt()方法。Unsafe類的大部分方法都是native的,用來像C語言一樣從底層操作內存

這個方法的var1和var2,就是根據對象偏移量得到在主內存的快照值var5。然後compareAndSwapInt方法通過var1和var2得到當前主內存的實際值。如果這個實際值快照值相等,那麼就更新主內存的值爲var5+var4。如果不等,那麼就一直循環,一直獲取快照,一直對比,直到實際值和快照值相等爲止。

比如有A、B兩個線程

  1. 一開始都從主內存中拷貝了原值爲3;
  2. A線程執行到var5=this.getIntVolatile,即var5=3。此時A線程掛起;
  3. B修改原值爲4,B線程執行完畢,由於加了volatile,所以這個修改是立即可見的;
  4. A線程被喚醒,執行this.compareAndSwapInt()方法,發現這個時候主內存的值不等於快照值3,所以繼續循環,重新從主內存獲取。
  5. 線程A重新獲取value值,因爲變量value被volatile修飾,所以其他線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直至成功。

ABA問題

所謂ABA問題,其實用最通俗易懂的話語來總結就是狸貓換太子

就是比較並交換的循環,存在一個時間差,而這個時間差可能帶來意想不到的問題。

比如有兩個線程A、B:

  1. 一開始都從主內存中拷貝了原值爲3;
  2. A線程執行到var5=this.getIntVolatile,即var5=3。此時A線程掛起;
  3. B修改原值爲4,B線程執行完畢;
  4. 然後B覺得修改錯了,然後再重新把值修改爲3;
  5. A線程被喚醒,執行this.compareAndSwapInt()方法,發現這個時候主內存的值等於快照值3,(但是卻不知道B曾經修改過),修改成功。

儘管線程A CAS操作成功,但不代表就沒有問題。有的需求,比如CAS,只注重頭和尾,只要首尾一致就接受。但是有的需求,還看重過程,中間不能發生任何修改,這就引出了AtomicReference原子引用。

AtomicReference原子引用

AtomicInteger對整數進行原子操作,如果是一個POJO呢?可以用AtomicReference來包裝這個POJO,使其操作原子化。

User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false

本質是比較的是兩個對象的地址是否相等。

使用場景:

一個線程使用student對象,另一個線程負責定時讀表,更新這個對象。那麼就可以用AtomicReference

AtomicStampedReference和ABA問題的解決

使用AtomicStampedReference類可以解決ABA問題。這個類維護了一個“版本號”Stamp,其實有點類似樂觀鎖的意思。

在進行CAS操作的時候,不僅要比較當前值,還要比較版本號。只有兩者都相等,才執行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

CAS總結

任何技術都不是完美的,當然,CAS也有他的缺點:

CAS實際上是一種自旋鎖,

  1. 一直循環,開銷比較大。
  2. 只能保證一個變量的原子操作,多個變量依然要加鎖。
  3. 引出了ABA問題(AtomicStampedReference可解決)。

而他的使用場景適合在一些併發量不高、線程競爭較少的情況,加鎖太重。但是一旦線程衝突嚴重的情況下,循環時間太長,爲給CPU帶來很大的開銷。

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