CAS 原子操做

理會CAS和CAS:java

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

  我通常會問面試官,問他問的CAS是"原子操做",仍是"單點登陸"算法

  由於在JAVA併發中的原子操做是稱爲CAS的,也就是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。編程

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

CAS(Compare And Swap):安全

  咱們先要學習的是併發編程中的CAS,也就是原子操做網絡

  那麼,什麼是原子操做?如何實現原子操做?併發

什麼是原子操做:ide

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

  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

內容關於:併發編程

本文來源於網絡,只作技術分享,一律不負任何責任

相關文章
相關標籤/搜索