1. 程式人生 > >入坑JAVA多執行緒併發(九)CAS和ABA

入坑JAVA多執行緒併發(九)CAS和ABA

  如果瞭解資料庫的悲觀鎖和樂觀鎖的話,對於理解CAS就很簡單了,因為CAS就是樂觀鎖的具體實現。
  悲觀鎖:在操作資料庫時本能的覺得一定會有競爭,所以把資料鎖住,不讓其他事物對對應的資料進行操作,在本次操作之後把鎖釋放,其它事物才可以進行操作。這個在java裡面就類似於synchronized。
  樂觀鎖:在操作資料庫時都覺得不會有其它事物和自己進行競爭,事物開始的時候就把對應的資料取出,在對資料進行更新的時候把資料和之前取出的資料進行對比,如果一樣說明沒有其他事物進行操作,可以進行操作,如果不一致,說明期間有其它事物對資料進行了操作,那麼這次操作失敗,進行重試。

1、CAS的三個值

V:要更新的變數
E:變數期望的值
N:變數要更新的值

這裡寫圖片描述
如上圖所示,如果V和E的值不符合,就重新取值進行操作,一直到相等執行更新或者符合退出條件為止。

2、原子操作類

有AtomicBoolean,AtomicInteger,AtomicLong,這裡以AtomicInteger的程式碼為例
如下程式碼:

public class Main {
    static volatile int cas = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            for
(int i = 0 ;i < 100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } cas++; } System.out.println(cas); }).start(); new
Thread(() -> { for (int i = 0 ;i < 100; i++){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } cas++; } System.out.println(cas); }).start(); } }

輸出為

173
173

測試多次結果都是小於200,這裡volatile只能保證可見性和指令重排。無法確保原子性,這裡cas++有三個操作,取出cas的值,加1,同步到主存。所以存線上程安全問題。
如果用AtomicInteger型別

public class Main {
    static AtomicInteger cas = new AtomicInteger(0);
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0 ;i < 100; i++){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                cas.incrementAndGet();
            }
            System.out.println(cas);
        }).start();
        new Thread(() -> {
            for (int i = 0 ;i < 100; i++){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                cas.incrementAndGet();
            }
            System.out.println(cas);
        }).start();

    }
}

輸出為

200
200

這裡是因為數太小,資料改大一點,第一個數就是小於200的,第二個數會一直等於預期的和,這裡就是用了CAS。

incrementAndGetf方法 (1.7和1.8版本)
//1.7版本
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
//CAS操作
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

//unsafe類的本地操作方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

//1.8版本
//AtomicInteger的incrementAndGet方法
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//CAS操作
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;
}

//獲取實時的value的值
public native int getIntVolatile(Object var1, long var2);

  compareAndSwapInt這個方法有四個引數,其中第一個引數為需要改變的物件,第二個為偏移量(即之前求出來的valueOffset的值),第三個引數為期待的值,第四個為更新後的值。整個方法的作用即為若呼叫該方法時,value的值與expect這個值相等,那麼則將value修改為update這個值,並返回一個true,如果呼叫該方法時,value的值與expect這個值不相等,那麼不做任何操作,並範圍一個false;
  1.7和1.8的不同在於CAS的操作返回值不一樣。1.7是直接返回加1後的值,1.8的CAS返回的是原始的值,只是在incrementAndGetf方法內把返回值進行加1再返回。
  CAS就是迴圈獲取value值,執行compareAndSwapInt方法。返回true就代表執行成功。退出迴圈。concurrent包很多類都是基於CAS實現的。
  其它方法包括getAndIncrement、getAndDecrement、getAndAdd、decrementAndGet等就是對int值進行加減運輸,內部實現原理和incrementAndGet是一樣的

3、ABA問題和AtomicStampedReference類

  ABA問題:CAS演算法實現一個重要前提需要取出記憶體中某時刻的資料,而在下時刻比較並替換,那麼在這個時間差類會導致資料的變化。
  舉個栗子:一個用單向連結串列實現的堆疊,棧頂為A,這時執行緒T1已經知道A.next為B,然後希望用CAS將棧頂替換為B,在T1執行之前,執行緒T2介入,將A、B出棧,再pushD、C、A。此時輪到執行緒T1執行CAS操作,檢測發現棧頂仍為A,所以CAS成功,棧頂變為B,但實際上B.next為null。具體如下圖
  這裡寫圖片描述
解決方法就是新增一個版本號。樂觀鎖的一種實現也是通過版本號實現,每一次操作都改變版本號,對比版本號確定是否執行操作。
JAVA中AtomicStampedReference就實現了這種版本標記的功能。

public class Main {

    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(0,1);
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0 ;i < 1000; i++){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stampedReference.compareAndSet(stampedReference.getReference(),stampedReference.getReference()+1,stampedReference.getStamp(),stampedReference.getStamp()+1);
            }
            System.out.println(stampedReference.getReference());
        }).start();
        new Thread(() -> {
            for (int i = 0 ;i < 1000; i++){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stampedReference.compareAndSet(stampedReference.getReference(),stampedReference.getReference()+1,stampedReference.getStamp(),stampedReference.getStamp()+1);
            }
            System.out.println(stampedReference.getReference());
        }).start();

    }
}

輸出為:

1970
1971

重複測試多次輸出一直是兩個小於2000的數字,因為兩個執行緒一起操作。會存線上程2在操作期間執行緒1已經把數字更新了,執行緒2就會操作失敗。所以最終的結果一定會和預期不符。