1. 程式人生 > 其它 >Java併發程式設計之CAS(比較並交換)

Java併發程式設計之CAS(比較並交換)

技術標籤:Java併發程式設計java多執行緒併發程式設計

文章目錄

1、compareAndSet方法

先以AtomicInteger為例看原始碼:

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

該原子類中的方法 compareAndSet(expect, update) 作用是:如果主實體記憶體的值與期望值 expect 相同,則將工作記憶體的值修改為 update 值後寫回主實體記憶體,並且方法返回 true ;反之如果主實體記憶體的值與期望值不同,則本次修改不成功,方法返回 false

public class CASDemo1 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 6) + "\t atomicInteger的值為" +atomicInteger);
        System.out.println(atomicInteger.compareAndSet(5, 7
)+ "\t atomicInteger的值為" +atomicInteger); } }

執行結果如下:

true	 atomicInteger的值為6
false	 atomicInteger的值為6

2、Unsafe類

從上面的原始碼中可以看到,compareAndSet 方法中呼叫了 unsafe 物件的 compareAndSwapInt 方法,而這個 unsafe 物件是屬於 Unsafe 類。

Java關鍵字之volatile 這篇文章裡面有提到採用 AtomicInteger.getAndIncrement() 可以解決 n++ 帶來的執行緒不安全問題,之所以能保證操作的原子性,也是因為呼叫了底層的 Unsafe類。

	/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

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

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

unsafe.getAndAddInt(this, valueOffset, 1) 中的 valueOffset 是該變數值在記憶體中的 地址偏移量 ,Unsafe 直接通過地址來精確獲取記憶體資料,從而保證了執行緒安全。

/*Unsafe類中的方法,var1 是操作的物件,如AtomicInteger物件
var2 是物件的地址偏移量
var4 是需要變動的資料大小,在getAndIncrement方法中就是固定為1,即加一
var5 是通過var1 var2找出的主記憶體中真實的值

如果var2 地址對應的值與var5相同,則對物件var1進行加var4操作後返回;
如果不同,則一直迴圈取值比較直到成功
*/
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;
    }

3、CAS是什麼

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

他的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的。

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

在上面的 getAndAddInt 方法中就採用了CAS進行值的修改,保證資料的執行緒安全性問題。

舉例:

假設執行緒A和執行緒B兩個執行緒同時執行getAndAddInt操作(分別跑在不同CPU上)

  1. AtomicInteger裡面的value原始值為3,即主記憶體中AtomicInteger的value為3,根據JMM模型,執行緒A和執行緒B各自持有一份值為3的value的副本分別到各自的工作記憶體。

  2. 執行緒A通過getIntVolatile(var1, var2)拿到value值3,這時執行緒A被掛起。

  3. 執行緒B也通過getlntVolatile(var1, var2)方法獲取到value值3,此時剛好執行緒B沒有被掛起並執行compareAndSwaplnt方法比較記憶體值也為3,成功修改記憶體值為4,執行緒B打完收工,一切OK。

  4. 這時執行緒A恢復,執行compareAndSwapInt方法比較,發現自己手裡的值數字3和主記憶體的值數字4不一致,說明該值已經被其它執行緒搶先一步修改過了,那A執行緒本次修改失敗,只能重新讀取重新來一遍了。

  5. 執行緒A重新獲取value值,因為變數value被volatle修飾,所以其它執行緒對它的修改,執行緒A總是能夠看到,執行緒A繼續執行compareAndSwapInt進行比較替換,直到成功。

    private volatile int value;		//AtomicInteger類中value被volatile修飾
    

4、CAS優缺點

優點:

  • 與 synchronized 相比來說,採用CAS可以提高併發量,因為它並沒有加鎖,允許多個執行緒同時訪問。

缺點:

  1. 可能出現迴圈時間長導致比較大的開銷(自旋鎖)
    由於CAS是一個do…while迴圈,如果比較不成功,則會持續進行取值比較,這樣會給CPU帶來很大的開銷。
  2. 只能保證一個共享變數的原子操作
    對多個共享變數操作時,迴圈CAS無法保證操作的原子性,這個時候就只能通過加鎖來保證原子性。
  3. 引發ABA問題

5、CAS存在的ABA問題

5.1 ABA的產生

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

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

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

5.2 原子引用

JDK已經提供了基本型別資料的原子類,如果自定義類也需要是原子的,那麼可以通過原子引用類 AtomicReference 來實現。

程式碼如下:

//自定義Person類
public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 //省略Get/Set和toString()   
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        
        Person p1 = new Person("zhangsan", 19);
        Person p2 = new Person("lisi", 30);

        AtomicReference<Person> reference = new AtomicReference<>();
        reference.set(p1);
        System.out.println(reference.compareAndSet(p1, p2) + "\t" + reference.get());
        System.out.println(reference.compareAndSet(p1, p2) + "\t" + reference.get());
    }
}

輸出結果:

true	Person{name='lisi', age=30}
false	Person{name='lisi', age=30}

5.2 ABA的解決辦法

採用帶有版本號的原子引用類可以徹底解決ABA問題,相當於為每次的CAS操作新增一個時間戳認證。

public class ABADemo {
    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger(10);
        AtomicStampedReference stampedReference = new AtomicStampedReference(10, 1);

        System.out.println("***********ABA產生************");

        new Thread(()->{
            System.out.println(atomicInteger.compareAndSet(10, 20) + "\t 執行緒A把atomicInteger的值修改為" + atomicInteger);
            System.out.println(atomicInteger.compareAndSet(20, 10)+ "\t 執行緒A把atomicInteger的值修改為" + atomicInteger);
        },"A").start();

        new Thread(()->{
            //暫停3秒執行緒,確保atomicInteger經過了執行緒A的兩次修改
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicInteger.compareAndSet(10, 30) + "\t 執行緒B把atomicInteger的值修改為" + atomicInteger);
        },"B").start();

        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("\n\n***********ABA解決************");
        new Thread(()->{
            int stamp1 = stampedReference.getStamp();    //獲取到初始版本號
            System.out.println(Thread.currentThread().getName() + "執行緒第一次拿到的版本號" + stamp1);
            //暫停1秒鐘,確保執行緒D也能拿到初始版本號
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            stampedReference.compareAndSet(10, 20, stamp1, stamp1 + 1);
            System.out.println(Thread.currentThread().getName() + "執行緒第一次修改stampedReference為20,當前版本號為" + stampedReference.getStamp());
            stampedReference.compareAndSet(20, 10, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "執行緒第二次修改stampedReference為10,當前版本號為" + stampedReference.getStamp());
        },"C").start();

        new Thread(()->{
            int stamp1 = stampedReference.getStamp();    //獲取到初始版本號
            System.out.println(Thread.currentThread().getName() + "執行緒第一次拿到的版本號" + stamp1);
            //暫停3秒鐘確保執行緒C完成一次ABA操作
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean b = stampedReference.compareAndSet(10, 30, stamp1, stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "執行緒第二次獲取到的版本號為" + stampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "執行緒第一次修改stampedReference的值為30" + (b ? "成功" : "失敗"));
        },"D").start();

    }
}

執行結果:

從下面的執行結果來看,採用帶有版本號的原子引用類就能有效解決ABA問題了。

***********ABA產生************
true	 執行緒A把atomicInteger的值修改為20
true	 執行緒A把atomicInteger的值修改為10
true	 執行緒B把atomicInteger的值修改為30


***********ABA解決************
C執行緒第一次拿到的版本號1
D執行緒第一次拿到的版本號1
C執行緒第一次修改stampedReference為20,當前版本號為2
C執行緒第二次修改stampedReference為10,當前版本號為3
D執行緒第二次獲取到的版本號為3
D執行緒第一次修改stampedReference的值為30失敗