欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java中CAS機(jī)制的原理與優(yōu)缺點(diǎn)

 更新時(shí)間:2023年06月29日 10:33:00   作者:后端碼匠  
CAS?英文就是?compare?and?swap?,也就是比較并交換,這篇文章主要來(lái)和大家介紹一下Java中CAS機(jī)制的原理與優(yōu)缺點(diǎn),感興趣的小伙伴可以了解一下

什么是 CAS 機(jī)制

CAS 英文就是 compare and swap ,也就是比較并交換,首先它是一個(gè)原子操作,可以避免被其他線程打斷。在Java并發(fā)中,最初接觸的應(yīng)該就是synchronized關(guān)鍵字了,但是synchronized屬于重量級(jí)鎖,很多時(shí)候會(huì)引起性能問(wèn)題,雖然在新的JDK中對(duì)其已經(jīng)進(jìn)行了優(yōu)化。volatile也是個(gè)不錯(cuò)的選擇,但是volatile不能保證原子性,只能在某些場(chǎng)合下使用。那么問(wèn)題來(lái)了,這個(gè) CAS 機(jī)制是怎么在不加鎖的情況下來(lái)保證共享資源的互斥呢?

synchronized補(bǔ)充:首先,synchronized會(huì)對(duì)第一個(gè)線程會(huì)有偏向,所以會(huì)給第一個(gè)線程添加偏向鎖,如果偏向鎖有其他線程來(lái)競(jìng)爭(zhēng)時(shí),這個(gè)鎖會(huì)升級(jí),變成輕量級(jí)鎖(多數(shù)情況下是自旋鎖),再如果這個(gè)鎖在一定次內(nèi)還是拿不到共享資源,這個(gè)輕量級(jí)的鎖會(huì)進(jìn)一步升級(jí),成為重量級(jí)鎖。

共享資源:通俗來(lái)講就是指那些可以被多個(gè)不同線程一起使用的數(shù)據(jù)。

首先先來(lái)看一個(gè)例子

當(dāng)有很多個(gè)線程同時(shí)訪問(wèn)一個(gè)貢獻(xiàn)資源時(shí),無(wú)法保證的貢獻(xiàn)資源只被一個(gè)線程使用,如果要保證只有一個(gè)線程使用時(shí),最先想到的一般就是加鎖。當(dāng)然先看不加鎖的情況:

    static volatile int NUMBER = 0; //volatile可以保證我們每次取值都是取內(nèi)存的值(最新值),而不是取棧里面的緩存值
    private static void add() throws InterruptedException {
        Thread.sleep(5); //模擬線程處理數(shù)據(jù)
        NUMBER++;
    }
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);//柵欄,底層為AQS實(shí)現(xiàn),當(dāng)里面值變?yōu)?時(shí),柵欄才打開(kāi)繼續(xù)運(yùn)行下面代碼
        Long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j<100; j++){
                        try {
                            add();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();//等待所有線程運(yùn)行完
        System.out.println("最后執(zhí)行結(jié)果:" + NUMBER + "。執(zhí)行時(shí)間為:" + (System.currentTimeMillis() - start) + "ms");
    }
//最后執(zhí)行結(jié)果:9637。執(zhí)行時(shí)間為:600ms

首先要剖析add方法里面的代碼,一方面在方法上加了鎖,導(dǎo)致線程只能串行化,而且線程在方法中sleep了5毫秒,才進(jìn)行NUMBER++,另一方面**在進(jìn)行NUMBER++中,NUMBER++也并非原子操作。**看到代碼的注釋:

    private synchronized static void add() throws InterruptedException {
        Thread.sleep(5);
       /**
         *  NUMBER++ 被拆分成三步
         *      1、從內(nèi)存中獲取 NUMBER 的值,賦給 A : A = NUMBER
         *      2、對(duì) A 進(jìn)行 +1 得到 B: B = A + 1
         *      3、把 B 的值寫(xiě)回 NUMBER:NUMBER = B
         * 在不加鎖的情況下,如果有A.B兩個(gè)線程同時(shí)執(zhí)行count++,他們通知執(zhí)行到上面步驟的第一步,得到count是一樣的,而當(dāng)?shù)?3 步操作結(jié)束后,count只加1,導(dǎo)致count結(jié)果不一致
         */
        NUMBER++;
    }
//最后執(zhí)行結(jié)果:10000。執(zhí)行時(shí)間為:56534ms

理清 JVM 的運(yùn)行過(guò)程,可以發(fā)現(xiàn)其實(shí)把鎖加在第三步就行,這樣可以保證原子性夠小,但是要怎么做呢?(重點(diǎn))

CAS(compare and swap) 就是一種樂(lè)觀鎖,比較和交換,原理是: 它有3個(gè)操作數(shù),內(nèi)存值 V,舊的期望值 expectNumber,要修改的新值 newNumber。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí)(比較),它就認(rèn)為這個(gè)期間沒(méi)有人來(lái)訪問(wèn)過(guò)這個(gè)貢獻(xiàn)資源。所以就把這個(gè)值改為新值(交換)。

static volatile int NUMBER = 0;//共享資源
    private static void add() throws InterruptedException {
        Thread.sleep(5);
        /**
         *  NUMBER++ 被拆分成三步
         *      1、從內(nèi)存中獲取 NUMBER 的值,賦給 A : A = NUMBER
         *      2、對(duì) A 進(jìn)行 +1 得到 B: B = A + 1
         *      3、把 B 的值寫(xiě)回 NUMBER:NUMBER = B
         *      升級(jí)第三步:
         *          1、獲取鎖
         *          2、獲取 NUMBER 的最新值,記作LV
         *          3、判斷 LV 是否等于 A,如果相等,則將 B 的值賦給NUMBER,并且返回true,如果不相等返回false,線程再自旋請(qǐng)求修改值
         */
       int expectNumber;
       for (;;)//自旋修改,知道修改成功
           if (compareAndSwap((expectNumber=getNumber()),expectNumber+1))
                    break;
    }
    /**
     * 模擬底層 sun.misc.Unsafe 的 compareAndSwap 方法,所以這里我加了鎖,底層并不是加鎖,它的底層是native修飾的,是方法區(qū)的方法,由C或者C++寫(xiě)的,它是對(duì)數(shù)據(jù)總線加鎖,synchronized是對(duì)字節(jié)碼加鎖。
     * @param expectNumber 期望值
     * @param newNumber 要交換的值
     * @return 返回交換是否成功
     */
    public static synchronized boolean compareAndSwap(int expectNumber, int newNumber){
        if (expectNumber == getNumber()){
            NUMBER = newNumber;
            return true;
        }
        return false;
    }
    public static int getNumber(){return NUMBER;}
//最后執(zhí)行結(jié)果:10000。執(zhí)行時(shí)間為:596ms

來(lái)看看 JDK 對(duì) CAS 的支持

首先,上面模擬的 compareAndSwap 其實(shí)是再 sun.misc.Unsafe 包里面的,JDK 并不希望開(kāi)發(fā)者去使用這個(gè)包,這個(gè)包里面有三個(gè)相關(guān)的方法

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
	//var1:表示要訪問(wèn)的對(duì)象,對(duì)象引用地址
	//var2:表示偏移量,即屬性再要訪問(wèn)對(duì)象地址的偏移量
	//var4:表示要修改的數(shù)據(jù)的期望值
	//var5:表示要修改為的新值

CAS 的實(shí)現(xiàn)原理

CAS 通過(guò) JNI(Java Native Interface)的代碼來(lái)實(shí)現(xiàn),允許java調(diào)用其他語(yǔ)言。而 compareAndSwap 的方法就是借用C語(yǔ)言來(lái)調(diào)用CPU底層的指令(cmpxchg)來(lái)實(shí)現(xiàn)的。cmpxchg是一個(gè)原子指令,這個(gè)指令是給數(shù)據(jù)總線進(jìn)行加鎖,所以是線程安全的。

CAS 其實(shí)也是有缺點(diǎn)的

1、首先就是經(jīng)典的ABA問(wèn)題

CAS需要在操作值的時(shí)候檢查下值有沒(méi)有發(fā)生變化,如果沒(méi)有發(fā)生變化則更新,但是如果一個(gè)值原來(lái)是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒(méi)有發(fā)生變化,但是實(shí)際上卻變化了。這就是CAS的ABA問(wèn)題。

2、就是循環(huán)時(shí)間長(zhǎng)開(kāi)銷大

如果CAS不成功,則會(huì)原地自旋,如果長(zhǎng)時(shí)間自旋會(huì)給CPU帶來(lái)非常大且沒(méi)必要的開(kāi)銷。

3、只能保證一個(gè)共享變量的原子操作

只能保證一個(gè)共享變量的原子操作。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖,或者有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來(lái)操作。比如有兩個(gè)共享變量 i=2,j=a,合并一下 ij=2a,然后用CAS來(lái)操作間。從Java1.5開(kāi)始DK提供了AtomicRefrence類來(lái)保證引用對(duì)象之間的原子性,你可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。

如何解決 ABA 問(wèn)題

常見(jiàn)的解決思路是使用版本號(hào)。在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加一,那么A-B-A 就會(huì)變成1A-2B-3A。 目前在JDK的atomic包里提供了一個(gè)類AtomicStampedReference來(lái)解決ABA問(wèn)題。這個(gè)類的compareAndSet方法作用是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。

//沒(méi)有版本號(hào)的情況
static AtomicInteger a = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
    System.out.println("初始值:"+a.get());
    new Thread(new Runnable() {
        @Override
        public void run() {
            int expectNum = a.get();//拿到最初的值
            int newNum = expectNum+1;
            try {
                Thread.sleep(1000); //先休眠一會(huì),保證是最后執(zhí)行的
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean isSuccess = a.compareAndSet(expectNum, newNum);
            System.out.println("主線程執(zhí)行結(jié)果:" + isSuccess + ",值為:" + a.get());
        }
    },"主線程").start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a.incrementAndGet();
            System.out.println("干擾線程執(zhí)行結(jié)果:"  + a.get());
            a.decrementAndGet();
            System.out.println("干擾線程執(zhí)行結(jié)果:"  + a.get());
        }
    },"干擾線程").start();
}
/*
初始值:1
干擾線程執(zhí)行結(jié)果:2
干擾線程執(zhí)行結(jié)果:1
主線程執(zhí)行結(jié)果:ture,值為:2*/

上面就是模擬的ABA情況,那么使用AtomicStampedReference可以怎么解決呢?先看到AtomicStampedReference里面關(guān)于創(chuàng)建對(duì)象和修改版本號(hào)和值的 API 。

    //initialRef為初始值,initialStamp為初始版本號(hào)
	public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
	/**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference             期望的引用
     * @param newReference the new value for the reference  		        引用的新值
     * @param expectedStamp the expected value of the stamp			期望的版本號(hào)
     * @param newStamp the new value for the stamp				新值的版本號(hào)
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&  
            expectedStamp == current.stamp &&
            //前兩個(gè)判斷當(dāng)前的版本或者當(dāng)前的值是否被改過(guò),改過(guò)就直接返回false
            ((newReference == current.reference &&
              newStamp == current.stamp) 
             //上面兩個(gè)判斷當(dāng)前的版本號(hào)和新值是否和要修改后的一致,一致就不進(jìn)行修改
             //如果不一致,就修改當(dāng)前的版本值和當(dāng)前值
             ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

由于每個(gè)過(guò)程值都會(huì)有對(duì)應(yīng)的版本,所以在修改過(guò)程中需要傳入期望版本和當(dāng)前的值,數(shù)據(jù)庫(kù)的多版本并發(fā)控制也類似,先來(lái)看一下修改后的代碼:

static AtomicStampedReference<Integer> a = new AtomicStampedReference(new Integer(1),1);
public static void main(String[] args) throws InterruptedException {
    System.out.println("初始值:"+a.getReference());
    new Thread(new Runnable() {
        @Override
        public void run() {
            //獲取需要的參數(shù)
            int expectReference = a.getReference();
            int newReference = expectReference + 1;
            int expectStamp = a.getStamp();
            int newStamp = expectStamp + 1;
            try {
                Thread.sleep(1000);//睡眠保證最后執(zhí)行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //傳入我們之前拿到的期望版本號(hào)和期望的值
            boolean isSuccess = a.compareAndSet(expectReference, newReference, expectStamp, newStamp);
            System.out.println("主線程執(zhí)行結(jié)果:" + isSuccess + ",值為:" + a.getReference());
        }
    },"主線程").start();
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //模擬修改ABA模式
            a.compareAndSet(a.getReference(),a.getReference()+1,a.getStamp(),a.getStamp()+1);
            System.out.println("干擾線程執(zhí)行結(jié)果:"  + a.getReference());
            a.compareAndSet(a.getReference(),a.getReference()-1,a.getStamp(),a.getStamp()+1);
            System.out.println("干擾線程執(zhí)行結(jié)果:"  + a.getReference());
        }
    },"干擾線程").start();
}
/*
初始值:1
干擾線程執(zhí)行結(jié)果:2
干擾線程執(zhí)行結(jié)果:1
主線程執(zhí)行結(jié)果:false,值為:1*/

可以看到執(zhí)行結(jié)果是false,也就是說(shuō)只要值被動(dòng)過(guò),就會(huì)修改失敗,但是版本號(hào)需要人工維護(hù)。當(dāng)然,如果對(duì)于業(yè)務(wù)的過(guò)程不是很注重的話也不需要去關(guān)注ABA問(wèn)題,也不需要去維護(hù)版本號(hào),而如果涉及到重要業(yè)務(wù)(轉(zhuǎn)賬),則需要解決ABA問(wèn)題。

如何解決循環(huán)時(shí)間長(zhǎng)開(kāi)銷大問(wèn)題

自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷。如果JVM能支持處理器提供的pause指令那么效率會(huì)有一定的提升,pause指令有兩個(gè)作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會(huì)消耗過(guò)多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零。第二它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執(zhí)行效率。

總結(jié)

  • CAS 可以實(shí)現(xiàn)原子性的操作,說(shuō)白了就是一個(gè)native的API,解決并發(fā)帶來(lái)的問(wèn)題,但是也存在一些自身的問(wèn)題。
  • 如何解決CAS自身帶來(lái)的問(wèn)題。
  • 明白他的應(yīng)用,在讀Concurrent包下的類的源碼時(shí),發(fā)現(xiàn)無(wú)論是ReenterLock內(nèi)部的AQS(后續(xù)會(huì)出博客講到),還是各種Atomic開(kāi)頭的原子類,內(nèi)部都應(yīng)用到了CAS。

到此這篇關(guān)于詳解Java中CAS機(jī)制的原理與優(yōu)缺點(diǎn)的文章就介紹到這了,更多相關(guān)Java CAS機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • IDEA中Maven依賴包無(wú)法下載或?qū)氲慕鉀Q方案(系統(tǒng)缺失文件導(dǎo)致)

    IDEA中Maven依賴包無(wú)法下載或?qū)氲慕鉀Q方案(系統(tǒng)缺失文件導(dǎo)致)

    在配置Maven環(huán)境時(shí),可能會(huì)遇到各種報(bào)錯(cuò)問(wèn)題,首先確保Maven路徑配置正確,例如使用apache-maven-3.5.0版本,則需要在系統(tǒng)環(huán)境變量的Path中添加其bin目錄路徑,并上移優(yōu)先級(jí),接下來(lái),在Maven的conf目錄下修改settings.xml文件,將鏡像源改為阿里云
    2024-09-09
  • java 序列化與反序列化的實(shí)例詳解

    java 序列化與反序列化的實(shí)例詳解

    這篇文章主要介紹了java 序列化與反序列化的實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下
    2017-07-07
  • mybatis-plus id主鍵生成的坑

    mybatis-plus id主鍵生成的坑

    這篇文章主要介紹了mybatis-plus id主鍵生成的坑,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-08-08
  • Java多線程——之一創(chuàng)建線程的四種方法

    Java多線程——之一創(chuàng)建線程的四種方法

    這篇文章主要介紹了Java創(chuàng)建線程方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • spring boot springjpa 支持多個(gè)數(shù)據(jù)源的實(shí)例代碼

    spring boot springjpa 支持多個(gè)數(shù)據(jù)源的實(shí)例代碼

    這篇文章主要介紹了spring boot springjpa 支持多個(gè)數(shù)據(jù)源的實(shí)例代碼,需要的朋友可以參考下
    2018-04-04
  • Java字符串拼接新方法 StringJoiner用法詳解

    Java字符串拼接新方法 StringJoiner用法詳解

    在本篇文章中小編給大家分享的是一篇關(guān)于Java字符串拼接新方法 StringJoiner用法詳解,需要的讀者們可以參考下。
    2019-09-09
  • 詳解Spring+Hiernate整合

    詳解Spring+Hiernate整合

    這篇文章主要介紹了詳解Spring+Hiernate整合,spring整合hibernate主要介紹以xml方式實(shí)現(xiàn),有興趣的可以了解一下。
    2017-04-04
  • Java設(shè)計(jì)模式之淺談外觀模式

    Java設(shè)計(jì)模式之淺談外觀模式

    這篇文章主要介紹了Java設(shè)計(jì)模式之外觀模式的相關(guān)資料,需要的朋友可以參考下
    2022-09-09
  • springboot+Vue實(shí)現(xiàn)分頁(yè)的示例代碼

    springboot+Vue實(shí)現(xiàn)分頁(yè)的示例代碼

    本文主要介紹了springboot+Vue實(shí)現(xiàn)分頁(yè)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-06-06
  • 如何在Maven項(xiàng)目配置pom.xml指定JDK版本和編碼

    如何在Maven項(xiàng)目配置pom.xml指定JDK版本和編碼

    maven是個(gè)項(xiàng)目管理工具,如果我們不告訴它要使用什么樣的jdk版本編譯,它就會(huì)用maven-compiler-plugin默認(rèn)的jdk版本來(lái)處理,這樣就容易出現(xiàn)版本不匹配的問(wèn)題,這篇文章主要給大家介紹了關(guān)于如何在Maven項(xiàng)目配置pom.xml指定JDK版本和編碼的相關(guān)資料,需要的朋友可以參考下
    2024-01-01

最新評(píng)論