CAS 原子操作

彼岸舞發表於2020-10-04

理會CAS和CAS:

  有時候面試官面試問你的時候,會問,談談你對CAS的理解,這時應該有很多人,就會比較懵,當然,我也會比較懵,當然我和很多人的懵不同,很多人可能,並不知道CAS是一個什麼東西,而在我看來我是不知道他問的是那個CAS

  我一般會問面試官,問他問的CAS是"原子操作",還是"單點登入"

  因為在JAVA併發中的原子操作是稱為CAS的,也就是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。

  但是在企業應用中CAS也被稱為企業級開源單點登入解決方案,是 Central Authentication Service 的縮寫 —— 中央認證服務,一種獨立開放指令協議,是 Yale 大學發起的一個企業級開源專案,旨在為 Web 應用系統提供一種可靠的 SSO 解決方案。

CAS(Compare And Swap):

  我們先要學習的是併發程式設計中的CAS,也就是原子操作

  那麼,什麼是原子操作?如何實現原子操作?

什麼是原子操作:

  原子,也是最小單位,是一個不可再分割的單位,不可被中斷的一個或者一系列操作

  CAS是以一種無鎖的方式實現併發控制,在實際情況下,同時操作一個物件的概率非常小,所以多數加鎖操作做的基本是無用功

  CAS以一種樂觀鎖的方式實現併發控制

如何實現原子操作:

  Java可以通過鎖和迴圈CAS的方式實現原子操作

為什麼要有CAS:  

  CAS就是比較並且替換的一個原子操作,在CPU的指令級別上進行保證

  為什麼要有CAS:

    Sync是基於阻塞的鎖的機制,

      1:被阻塞的執行緒優先順序很高

      2:拿到鎖的執行緒一直不釋放鎖則麼辦

      3:大量的競爭,消耗CPU,同時帶來死鎖或者其他執行緒安全

    因為通過鎖實現原子操作時,其他執行緒必須等待已經獲得鎖的執行緒執行完車之後才能獲取鎖,這樣就會佔用系統大量資源

CAS原理:

  從CPU指令級別保證這是一個原子操作

CAS包含哪些引數:

  三個運算子:

    一個記憶體地址V

    一個期望的值A

    一個新值B

  基本思路:

    如果地址V上的值和期望的值A相等,就給地址V賦值新值B,如果不是,不做任何操作

  迴圈CAS:

    在一個(死)迴圈中[for(;;)]裡不斷進行CAS操作,直到成功為止(自旋操作即死迴圈)

CAS問題:

  ABA問題:

    那麼什麼是ABA問題?就是記憶體中原本是A,然後通過CAS變成了B,然後再次通過CAS變成了A,這個過程中,相對於結果來說,是沒有任何改變的,但是相對於記憶體來說,至少發生過兩次變化,這就是ABA問題

    生活中:

      就像你接了一杯水,這時水是滿的,但是這個時候,你的同時很渴,過來拿你的水直接喝掉了一半,這時水剩下了一半,接著,你的同事又重新把你的水幫你接滿了,那麼這時你的水還是滿的,相對於水來說,他還是滿的,但是相對於杯子來說,他已經被用過了兩次,一次是喝水,一次是接水,這就是ABA問題

    從Java1.5開始JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

    生活中:

      你接了一杯水,然後旁邊放上一張登記表,這個時候你同事過來,直接喝掉了一半,然後登記上,XXX喝掉了一半的水,然後去給你接滿了,再次登記上,我給你接滿了,這時,ABA的問題就得到了解決,你一看這個表就知道了一切

  開銷問題:

    在自旋或者死迴圈中不斷進行CAS操作,但是長期操作不成功,CPU不斷的迴圈,帶來的開銷問題

    自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

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

    當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。

CAS的目的:

  利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞演算法,J.U.C在效能上有了很大的提升。

JDK中相關原子操作類的使用:

  更新基本型別類:AtomicBoolean,AtomicInteger,AtomicLong

  更新陣列類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArrat

  更新引用型別:AtomicReference,AtomicMarkableReference,AtomicStampedReference

  原子更新欄位類:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater

理論已經理解的差不多了,接下來寫寫程式碼

使用AtomicInteger

package org.dance.day3;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用原子類int型別
 * @author ZYGisComputer
 */
public class UseAtomicInt {

    static AtomicInteger atomicInteger = new AtomicInteger(10);

    public static void main(String[] args) {
        // 10->11 10先去再增加
        System.out.println(atomicInteger.getAndIncrement());
        // 11->12 12先增加再取
        System.out.println(atomicInteger.incrementAndGet());
        // 獲取
        System.out.println(atomicInteger.get());
    }

}

返回值:

10
12
12

通過返回值可以看到,第一個是先獲取返回值後累加1,第二個是先累加1後再返回,第三個是獲取當前值

使用AtomicIntegerArray

package org.dance.day3;

import java.util.concurrent.atomic.AtomicIntegerArray;

/**
 * 使用原子類int[]
 * @author ZYGisComputer
 */
public class UseAtomicIntegerArray {

    static int[] values = new int[]{1,2};

    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(values);

    public static void main(String[] args) {
        //改變的第一個引數是 陣列的下標,第二個是新值
        atomicIntegerArray.getAndSet(0,3);
        // 獲取原子陣列類中的下標為0的值
        System.out.println(atomicIntegerArray.get(0));
        // 獲取源陣列中下標為0的值
        System.out.println(values[0]);
    }

}

返回結果:

3
1

通過返回結果我們可以看到,源陣列中的值並沒有改變,只有引用中的值發生了改變,這是則麼回事?

/**
     * Creates a new AtomicIntegerArray with the same length as, and
     * all elements copied from, the given array.
     *
     * @param array the array to copy elements from
     * @throws NullPointerException if array is null
     */
    public AtomicIntegerArray(int[] array) {
        // Visibility guaranteed by final field guarantees
        this.array = array.clone();
    }

通過看原始碼我們得知他是呼叫了陣列的克隆方法,克隆了一個一模一樣的

使用AtomicReference

package org.dance.day3;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 使用原子類引用型別
 * @author ZYGisComputer
 */
public class UseAtomicReference {

    static AtomicReference<UserInfo> atomicReference = new AtomicReference<>();

    public static void main(String[] args) {

        UserInfo src = new UserInfo("彼岸舞",18);

        // 使用原子引用類包裝一下
        atomicReference.set(src);

        UserInfo target = new UserInfo("彼岸花",19);

        // 這裡就是CAS改變了,這個應用類就好像一個容器也就是記憶體V,而src就是原值A,target就是新值B
        // 期望原值是src,如果是的話,改變為target,否則不變
        atomicReference.compareAndSet(src,target);

        System.out.println(atomicReference.get());

        System.out.println(src);

    }

    static class UserInfo{
        private String name;
        private int age;

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }

        public UserInfo() {
        }

        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

}

返回結果:

UserInfo{name='彼岸花', age=19}
UserInfo{name='彼岸舞', age=18}

通過返回結果可以直觀的看到,原子引用類中的值發生了改變,但是源物件src卻沒有改變,因為原子引用類和原物件本身是兩個東西,CAS後就可以理解為記憶體中的東西變了,也可以說是引用變了,他只能保證你在改變這個引用的時候保證是原子性的

記得之前上面說的ABA問題吧,在這裡就是解決程式碼

JDK中提供了兩種解決ABA問題的類

  AtomicStampedReference

    AtomicStampedReference,裡面是用int型別,他關心的是被人動過幾次

  AtomicMarkableReference

    AtomicMarkableReference,裡面是用boolean型別,他只關心這個版本有沒有人動過

 兩個類關心的點不一樣,側重的方向不一樣,就像之前說的喝水問題,AtomicStampedReference關心的是,被幾個人動過,而AtomicMarkableReference關心的是有沒有人動過

使用AtomicStampedReference解決ABA問題

package org.dance.day3;

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * 使用版本號解決ABA問題
 * @author ZYGisComputer
 */
public class UseAtomicStampedReference {

    /**
     * 構造引數地第一個是預設值,第二個就是版本號
     */
    static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>("src",0);

    public static void main(String[] args) throws InterruptedException {

        // 獲取初始版本號
        final int oldStamp = atomicStampedReference.getStamp();

        // 獲取初始值
        final String oldValue = atomicStampedReference.getReference();

        System.out.println("oldValue:"+oldValue+" oldStamp:"+oldStamp);

        Thread success = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+",當前變數值:"+oldValue+"當前版本號:"+oldStamp);
                // 變更值和版本號
                /**
                 * 第一個引數:期望值
                 * 第二個引數:新值
                 * 第三個引數:期望版本號
                 * 第四個引數:新版本號
                 */
                boolean b = atomicStampedReference.compareAndSet(oldValue, oldValue + "java", oldStamp, oldStamp + 1);
                System.out.println(b);
            }
        });

        Thread error = new Thread(new Runnable() {
            @Override
            public void run() {
                // 獲取原值
                String sz = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+",當前變數值:"+sz+"當前版本號:"+stamp);
                boolean b = atomicStampedReference.compareAndSet(oldValue, oldValue + "C", oldStamp, oldStamp + 1);
                System.out.println(b);
            }
        });

        success.start();
        success.join();
        error.start();
        error.join();
        System.out.println(atomicStampedReference.getReference()+":"+atomicStampedReference.getStamp());
    }

}

返回結果:

oldValue:src oldStamp:0
Thread-0,當前變數值:src當前版本號:0
true
Thread-1,當前變數值:srcjava當前版本號:1
false
srcjava:1

通過返回結果可以觀察到,原始值是src,版本是0,然後使用join方法使我們的正確執行緒確保咋錯誤執行緒之前執行完畢,當正確執行緒執行完畢後,會把值改為srcjava,版本改為+1,然後執行錯誤的執行緒,錯誤的執行緒在嘗試去改值的時候,發現期望的值是src,但是值已經被改變成srcjava了,並且期望的版本是0,但是版本已經被改為1了,所以他無法修改,在兩個執行緒都執行完畢之後,列印的值是 srcjava,版本是1,成功的解決了ABA問題,當然在這裡面我的期望值是還是src,也可以改為src+java但是因為版本不一樣也是無法修改成功的;親測沒問題

原子更新欄位類就不寫了,那個使用比較麻煩,如果多個欄位的話,就直接使用AtomicReference類就可以了

作者:彼岸舞

時間:2020\10\04

內容關於:併發程式設計

本文來源於網路,只做技術分享,一概不負任何責任

相關文章