CAS底層原理與ABA問題

Raicho發表於2020-07-17

CAS定義

CAS(Compare And Swap)是一種無鎖演算法。CAS演算法是樂觀鎖的一種實現。CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當預期值A和記憶體值V相同時,將記憶體值V修改為B並返回true,否則返回false。

CAS與synchronized

(1)synchronized加鎖,同一時間段只允許一個執行緒訪問,能夠保證一致性但是併發性下降。

(2)CAS是一個自旋鎖演算法,使用do-while不斷判斷(沒有加鎖),保證一致性和併發性,但是比較消耗CPU資源。使用CAS就可以不用加鎖來實現執行緒安全。

  • 原子性保證:CAS演算法依賴於rt.jar包下的sun.misc.Unsafe類,該類中的所有方法都是native修飾的,直接呼叫作業系統底層資源執行相應的任務。
  • 記憶體可見性和禁止指令重排序的保證:AtomicXxx類中的成員變數value是由volatile修飾的:private volatile int value;

CAS演算法的缺點

CAS雖然很高效的解決了原子操作問題,但是CAS仍然存在三大問題。

  • 迴圈時間長、開銷很大。

當某一方法比如:getAndAddInt執行時,如果CAS失敗,會一直進行嘗試。如果CAS長時間嘗試但是一直不成功,可能會給CPU帶來很大的開銷。

  • 只能保證一個共享變數的原子操作。

當操作1個共享變數時,我們可以使用迴圈CAS的方式來保證原子操作,但是操作多個共享變數時,迴圈CAS就無法保證操作的原子性,這個時候就需要用鎖來保證原子性。

  • 存在ABA問題

如果一個執行緒在初次讀取時的值為A,並且在賦值的時候檢查該值仍然是A,但是可能在這兩次操作,之間有另外一個執行緒現將變數的值改成了B,然後又將該值改回為A,那麼CAS會誤認為該變數沒有變化過。

CAS底層原理

sum.misc.Unsafe類中有多個方法被native關鍵字標記,這說明該方法是原生態的方法,它是一個呼叫非java語言的介面,也就是說這個介面的實現是其他語言實現的。CAS併發原語就是體現在java的sum.misc.Unsafe類中的各個方法,呼叫這個類中的CAS方法JVM就會通過其他語言生成若干條系統指令,完整這些指令的過程中,是不允許被中斷的,所以CAS是一條CUP的原子指令,所以它不會造成資料不一致問題。

多執行緒情況下,number變數每次++都會出現執行緒安全問題,AtomicInteger則不會,因為它保證了原子性。

 我們進去看,getAndIncrement呼叫的就是Unsafe類中的getAndAddInt方法,this表示當前物件,valueOffset表示變數值在記憶體中的偏移量(也就是記憶體地址)

我們再進入Unsafe類看看var1就是getAndIncrement方法傳過來的物件,var2是系統偏移量,這裡是使用了do-while迴圈,一開始迴圈就通過var1物件和var2偏移量獲取期望值var5,進入迴圈,compareAndSwapInt方法被native關鍵字標記的,所以他是原子性的 ,var2的值與var的值相等時,則使用新的值var5+var4,返回true,迴圈條件取反則結束迴圈,否則如果var2與var5不相等就繼續迴圈,直到條件不滿足再跳出迴圈

// unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 獲取物件var1,偏移量為var2地址上的值,並賦值給var5
        var5 = this.getIntVolatile(var1, var2);
        /**
         * 再次獲取物件var1,偏移量var2地址上的值,並和var5進行比較:
         * - 如果不相等,返回false,繼續執行do-while迴圈
         * - 如果相等,將返回的var5數值和var4相加並返回
         */
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    // 最終總是返回物件var1,偏移量為var2地址上的值,即上述所說的V。
    return var5;
}

ABA問題解決方案

使用AtomicStampedReference或者AtomicMarkableReference來解決CAS的ABA問題,思路類似於SVN版本號,SpringBoot熱部署中trigger.txt

AtomicStampedReference解決方案:每次修改都會讓stamp值加1,類似於版本控制號

package com.raicho.mianshi.mycas;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author: Raicho
 * @Description:
 * @program: mianshi
 * @create: 2020-07-17 10:19
 **/
public class AtomicStampedReferenceABA {
    private static AtomicReference<Integer> ar = new AtomicReference<>(0);
    private static AtomicStampedReference<Integer> asr =
            new AtomicStampedReference<>(0, 1);

    public static void main(String[] args) {
        System.out.println("=============演示ABA問題(AtomicReference)===========");
        new Thread(() -> {
            ar.compareAndSet(0, 1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ar.compareAndSet(1, 0);
            System.out.println(Thread.currentThread().getName() + "進行了一次ABA操作");
        }, "子執行緒").start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        boolean res = ar.compareAndSet(0, 100);
        if (res) {
            System.out.println("main成功修改, 未察覺到子執行緒進行了ABA操作");
        }

        System.out.println("=============解決ABA問題(AtomicStampReference)===========");
        new Thread(() -> {
            int curStamp = asr.getStamp();
            System.out.println("t1獲取當前stamp: " + curStamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            asr.compareAndSet(0, 1, curStamp, curStamp + 1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            asr.compareAndSet(1, 0, asr.getStamp(), asr.getStamp() + 1);
        }, "t1").start();

        new Thread(() -> {
            int curStamp = asr.getStamp();
            System.out.println("t2獲取當前stamp: " + curStamp);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = asr.compareAndSet(0, 100, curStamp, curStamp + 1);
            if (!result) {
                System.out.println("修改失敗! 預期stamp: " + curStamp + ", 實際stamp: " + asr.getStamp());
            }
        }, "t2").start();
    }
}

執行結果:

AtomicMarkableReference:如果不關心引用變數中途被修改了多少次,而只關心是否被修改過,可以使用AtomicMarkableReference:

package com.raicho.mianshi.mycas;

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * @author: Raicho
 * @Description:
 * @program: mianshi
 * @create: 2020-07-17 10:46
 **/
public class AtomicMarkableReferenceABA {
    private static AtomicMarkableReference<Integer> amr = new AtomicMarkableReference<>(0, false);

    public static void main(String[] args) {
        new Thread(() -> {
            amr.compareAndSet(0, 1, false, true);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            amr.compareAndSet(1, 0, true, true);
            System.out.println("子執行緒進行了ABA修改!");
        }, "子執行緒").start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        boolean res = amr.compareAndSet(0, 100, false, true);
        if (!res) {
            System.out.println("修改失敗! 當前isMarked: " + amr.isMarked());
        }
    }
}

執行結果:

參考

知乎:https://zhuanlan.zhihu.com/p/93418208

csdn:https://blog.csdn.net/justry_deng/article/details/83449038

相關文章