CAS你知道嗎?底層如何實現?ABA問題又是什麼?關於這些你知道答案嗎

y浴血發表於2021-07-06

CAS你知道嗎?如何實現?

1. compareAndSet

volatile當中我們提到,volatile不能保證原子語義,所以當用到變數自增時,如果用到synchronized會太”重“了,在多執行緒環境下我們一般用原子類如AtomicInteger,其底層是CAS,volatile見此篇

public final boolean compareAndSet(int expect, int update) {
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

上述程式碼表示:

  • 如果執行緒的期望值和實體記憶體的真實值一樣,那麼就修改為更新值
  • 如果不一樣,本次修改失敗,就需要重新獲取主實體記憶體的值

簡單的程式碼例子:

package com.yuxue.juc.CASTest;

import java.util.concurrent.atomic.AtomicInteger;
/**
 * CAS:比較並交換
 */
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        //compareAndSet返回的是boolean型別,修改成功返回true,失敗返回false
        System.out.println(atomicInteger.compareAndSet(0, 666) + "\t current data is " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(1, 777) + "\t current data is " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(666, 888) + "\t current data is " + atomicInteger.get());
    }
}

輸出為:

//第一次期望值是0,原值預設0,所以CAS成功修改為666
true	 current data is 666
//第二次期望值是1,原值第一步修改為666,所以CAS不成功修改
false	 current data is 666
//第三次期望值是666,原值第一步修改為666,所以CAS成功修改為888
true	 current data is 888

2. CAS底層原理?對Unsafe的理解

我們都知道,atomicInteger.getAndIncrement()方法能夠在多執行緒環境下保證變數的安全同時讓其自增,但是原始碼當中也沒有synchronized,那麼如何保證底層安全?如果保證多執行緒環境下的變數安全?我們開啟其原始碼:

2.1 compareAndSet

public final boolean compareAndSet(int expect, int update) {
  return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

裡面有用到unsafe物件的compareAndSwapInt方法,再找unsafe原始碼

其底層用到Unsafe類來保證執行緒安全!

2.2 Unsafe

  • 是CAS核心類,由於Java方法無法直接訪問地層系統,需要通過本地(native)方法來訪問,Unsafe相當 於一個後門,基於該類可以直接操作特定記憶體資料。Unsafe類存在於sun.misc 包中,其內部方法操作可 以像C的指標一樣直接操作記憶體,因為Java中CAS操作的執行依賴於Unsafe類的方法。

  • Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接呼叫作業系統底層資源執行相應任務

  • 變數valueOffset,表示該變數值在記憶體中的偏移地址,因為Unsafe就是根據記憶體偏移地址獲取資料的

  • 變數value用volatile修飾,保證多執行緒之間的可見性

    image-20210706102129437

2.3 CAS是什麼

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

  • 他的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的
  • CAS併發原語體現在JAVA語言中就是sun.misc.Unsafe類中各個方法。呼叫Unsafe類中的CAS方法,JVM會幫我們實現CAS彙編指令。這是一種完全依賴於硬體的功能,通過他實現了原子操作。
  • 由於CAS是一種系統原語,原語屬於作業系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成資料不一致問題

所以!CAS是通過Unsafe類的CPU指令源語來保證資料的原子性!

CAS原始碼:

//unsafe.getAndAddInt
//var1對應
public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
    v = getIntVolatile(o, offset);
  } while (!compareAndSwapInt(o, offset, v, v + delta));
  return v;
}

image-20210706103121530

o this即AtomicInteger物件本身

offset 該物件的引用地址(偏移地址)

delta 需要增加的變數

v通過AtomicInteger物件本身的offset偏移地址找出的主記憶體中真實的值,用該物件前的值與v比較; 如果相同,更新v+delta並且返回true, 如果不同,繼續去之然後再比較,直到更新完成

2.4 總結

CAS:比較當前工作記憶體中的值和主實體記憶體中的值,如果相同則執行規定操作,否則的話繼續比較直到主記憶體和工作記憶體中的值一值!(不清楚工作記憶體以及主記憶體的請移步檢視volatile中的JMM模型

3. CAS缺點

3.1 迴圈時間長,開銷大

例如getAndAddInt方法執行,有個do...while迴圈,如果CAS失敗,一直會進行嘗試,如果CAS長時間不成功, 可能會給CPU帶來很大的開銷(自旋!)

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

對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性

3.3 ABA問題

CAS演算法實現一個重要前提需要去除記憶體中某個時刻的資料並在當下時刻比較並替換,那麼在這個時間差類會導致資料的變化

例:比如執行緒1從記憶體位置V取出A,執行緒2同時也從記憶體取出A,並且執行緒2進行一些操作將值改為B,然後執行緒2又將V位置資料改成A,這時候執行緒1進行CAS操作發現記憶體中的值依然時A,然後執行緒1操作成功

儘管執行緒1的CAS操作成功,但是不代表這個過程沒有問題

4. 原子類AtomicInteger的ABA問題?原子更新引用?

首先我們已經闡述了ABA的概念以及問題,首先我們要知道原子引用類的概念

4.1 原子引用

AtomicReference<V>

這裡的V只要是其他的類均可使用AtomicReference作為其包裝類

示例程式碼:

package com.yuxue.juc.CASTest;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.concurrent.atomic.AtomicReference;

@Data
@NoArgsConstructor
@AllArgsConstructor
class User{
    private String name;
    private int age;
}
/**
 * 測試原子引用類
 * */
public class AtomicReferenceTest {
    public static void main(String[] args) {
        User u1 = new User("張三",18);
        User u2 = new User("李四",23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(u1);
        System.out.println(atomicReference.compareAndSet(u1, u2) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(u1, u2) + "\t" + atomicReference.get().toString());
    }
}

輸出結果為:

true	User(name=李四, age=23)
false	User(name=李四, age=23)

4.2 ABA問題程式碼實現

解決方案:帶時間戳的原子引用

首先ABA問題程式碼展示:

package com.yuxue.juc.CASTest;

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

public class ABASolution {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {
        new Thread(() -> {
            //ABA問題
            System.out.println(atomicReference.compareAndSet(100, 101) + "\t" + Thread.currentThread().getName() + "value is:" + atomicReference.get());
            System.out.println(atomicReference.compareAndSet(101, 100) + "\t" + Thread.currentThread().getName() + "value is:" + atomicReference.get());
        }, "t1").start();

        new Thread(() -> {
            //先休眠,讓t1執行緒完成ABA操作
            try {
                Thread.sleep(1000);
                System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + Thread.currentThread().getName() + "value is:" + atomicReference.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

結果為:

true	t1 value is:101
true	t1 value is:100
true	t2 value is:2019

可以看到t1首先修改值為101,之後又修改回來100,但是執行緒t2的工作記憶體中還是100,之後與主記憶體相比,發現主記憶體值也是100,之後放心修改值為2019,此時就會出現ABA問題

4.3 所以?怎麼解決ABA問題?

採用內建的類AtomicStampedReference<V>其為攜帶時間戳的類,我們可以每次更改值時對時間戳進行操作,這樣就可以保證不會出現ABA問題

package com.yuxue.juc.CASTest;

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

public class ABASolution {
    //建立變數,第一個是initialRef為初始值,第二個是initialStamp為初始化時間戳
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        //atmoinReferenceMethod();
        new Thread(() -> {
            //獲得時間戳,此時為1
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號" + stamp);
            try {
                //此處休眠的目的是為了讓t2獲得初始版本號
                Thread.sleep(1000);
                //第一次修改,值改為101,版本號加1
                atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t第2次版本號" + atomicStampedReference.getStamp());
                //第二次修改,值改為100,版本號加1
                atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                System.out.println(Thread.currentThread().getName() + "\t第3次版本號" + atomicStampedReference.getStamp());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();

        new Thread(() -> {
            //獲得時間戳,此時為1
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號" + stamp);
            try {
                //休眠2秒,此時執行緒t1已經將值改變但是又變回來,為ABA問題
                Thread.sleep(2000);
                //首先嚐試是否可以根據值和時間戳進行更改
                boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
                System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t當前最新實際版本號" + atomicStampedReference.getStamp());
                System.out.println(Thread.currentThread().getName() + "\t當前最新實際值" + atomicStampedReference.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

輸出結果:

t1	第1次版本號1
t2	第1次版本號1
//t1修改兩次,版本號加了2
t1	第2次版本號2
t1	第3次版本號3
//t2判斷版本號,之後再決定能不能改
t2	修改是否成功false	當前最新實際版本號3
//實際並沒有進行更改
t2	當前最新實際值100

這樣,我們用JUC內建atomic下的AtomicStampedReference類來解決了ABA問題

相關文章