Java并發(fā)編程:什么是CAS?這回總算知道了

無(wú)鎖的思想

眾所周知,Java中對(duì)并發(fā)控制的最常見方法就是鎖,鎖能保證同一時(shí)刻只能有一個(gè)線程訪問臨界區(qū)的資源,從而實(shí)現(xiàn)線程安全。然而,鎖雖然有效,但采用的是一種悲觀的策略。它假設(shè)每一次對(duì)臨界區(qū)資源的訪問都會(huì)發(fā)生沖突,當(dāng)有一個(gè)線程訪問資源,其他線程就必須等待,所以鎖是會(huì)阻塞線程執(zhí)行的。

當(dāng)然,凡事都有兩面,有悲觀就會(huì)有樂觀。而無(wú)鎖就是一種樂觀的策略,它假設(shè)線程對(duì)資源的訪問是沒有沖突的,同時(shí)所有的線程執(zhí)行都不需要等待,可以持續(xù)執(zhí)行。如果遇到?jīng)_突的話,就使用一種叫做CAS (比較交換) 的技術(shù)來鑒別線程沖突,如果檢測(cè)到?jīng)_突發(fā)生,就重試當(dāng)前操作到?jīng)]有沖突為止。

CAS概述

CAS的全稱是 Compare-and-Swap,也就是比較并交換,是并發(fā)編程中一種常用的算法。它包含了三個(gè)參數(shù):V,A,B。

其中,V表示要讀寫的內(nèi)存位置,A表示舊的預(yù)期值,B表示新值

CAS指令執(zhí)行時(shí),當(dāng)且僅當(dāng)V的值等于預(yù)期值A(chǔ)時(shí),才會(huì)將V的值設(shè)為B,如果V和A不同,說明可能是其他線程做了更新,那么當(dāng)前線程就什么都不做,最后,CAS返回的是V的真實(shí)值。

而在多線程的情況下,當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí),只有一個(gè)會(huì)成功并更新值,其余線程均會(huì)失敗,但失敗的線程不會(huì)被掛起,而是不斷的再次循環(huán)重試。正是基于這樣的原理,CAS即時(shí)沒有使用鎖,也能發(fā)現(xiàn)其他線程對(duì)當(dāng)前線程的干擾,從而進(jìn)行及時(shí)的處理。

CAS的應(yīng)用類

Java中提供了一系列應(yīng)用CAS操作的類,這些類位于java.util.concurrent.atomic包下,其中最常用的就是AtomicInteger,該類可以看做是實(shí)現(xiàn)了CAS操作的Integer,所以,下面我們就通過學(xué)習(xí)該類的案例來一窺全貌CAS的妙用。

學(xué)習(xí)AtomicInteger之前,我們先來看一段代碼實(shí)例:

public class AtomicDemo {

    public static int NUMBER = 0;

    public static void increase() {
        NUMBER++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo test = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++)
                    test.increase();
            }).start();
        }
        Thread.sleep(200);
        System.out.println(test.NUMBER);
    }
}

在main函數(shù)中開啟了10個(gè)線程,執(zhí)行后會(huì)輪流調(diào)用 increase(),當(dāng)然我們知道,運(yùn)行后輸出的結(jié)果肯定不是我們期望的值,因?yàn)闆]有做線程安全的處理,所以10個(gè)線程流量操作臨界區(qū)的資源NUMBER就會(huì)出錯(cuò)。

解決辦法并不難,用我們之前學(xué)過的鎖,例如synchronized修飾代碼塊,程序就會(huì)正常輸出10000。當(dāng)然,用鎖解決并不是我們想要的方式,因?yàn)殒i會(huì)阻塞線程,影響程序的性能,這時(shí)候,AtomicInteger就可以派上用場(chǎng)了。

將上面的程序改造一下,變成下面這樣:

public static AtomicInteger NUMBER = new AtomicInteger(0);

public static void increase() {
    NUMBER.getAndIncrement();
}

public static void main(String[] args) throws InterruptedException {
    AtomicDemo test = new AtomicDemo();
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            for (int j = 0; j < 1000; j++)
                test.increase();
        }).start();
    }
    Thread.sleep(200);
    System.out.println(test.NUMBER);
}

運(yùn)行main方法,程序輸出的就是我們想要的值,也就是10000。

上面的代碼中,increase方法里調(diào)用了NUMBER.getAndIncrement() ,這是AtomicInteger的自增方法,會(huì)對(duì)當(dāng)前的值加1,并且返回舊值,點(diǎn)進(jìn)方法的源碼,它調(diào)用的是unsafe.getAndAddInt()方法:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

getAndAddInt的作用是對(duì)當(dāng)前值加1,并返回舊值。

unsafe是Unsafe類的一個(gè)變量,通過Unsafe.getUnsafe()來獲取

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe類是一個(gè)比較特殊的類,它是一個(gè)JDK內(nèi)部使用的專屬類,用一般的編輯器無(wú)法直接查看源碼,只能看到反編譯后的class文件。

這里要擴(kuò)展一個(gè)知識(shí)點(diǎn),就是Java本身無(wú)法訪問操作系統(tǒng),需要使用native方法,而Unsafe類中的方法就包含了大量的native方法,提高了Java對(duì)系統(tǒng)底層的原子操作能力。例如我們代碼中使用到的getAndAddInt()底層就是調(diào)用一個(gè)native方法,用idea點(diǎn)擊方法,得到下面反編譯后的代碼:

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;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

compareAndSwapInt的作用是比較并交換整數(shù)值,如果指定的字段的值等于期望值,也就是CAS中的 'A' (預(yù)期值),那么就會(huì)把它設(shè)置為新值 (CAS中的 'B'),不難想象,該方法內(nèi)部的實(shí)現(xiàn)必然是依靠原子操作完成的。除此之外,Unsafe類中還提供了其他的原子操作的方法,例如上面源碼中的getIntVolatile就是使用volatile語(yǔ)義獲得給定對(duì)象的值,這些方法通過底層的原子操作高效的提升了應(yīng)用層面的性能。

CAS的缺點(diǎn)

雖然CAS的性能比起鎖要強(qiáng)大很多,但它也存在一些缺點(diǎn),例如:

1、循環(huán)的時(shí)間開銷大

在getAndAddInt的方法中,我們可以看到,只是簡(jiǎn)單的設(shè)置一個(gè)值卻調(diào)用了循環(huán),如果CAS失敗,會(huì)一直進(jìn)行嘗試。如果CAS長(zhǎng)時(shí)間不成功,那么循環(huán)就會(huì)不停的跑,無(wú)疑會(huì)給系統(tǒng)造成很大的開銷。

2、ABA問題

前面說過,CAS判斷變量操作成功的條件是V的值和A是一致的,這個(gè)邏輯有個(gè)小小的缺陷,就是如果V的值一開始為A,在準(zhǔn)備修改為新值前的期間曾經(jīng)被改成了B,后來又被改回為A,經(jīng)過兩次的線程修改對(duì)象的值還是舊值,那么CAS操作就會(huì)誤任務(wù)該變量從來沒被修改過。這就是CAS中的“ABA”問題。

當(dāng)然,"ABA"問題也有解決方案,Java并發(fā)包中提供了一個(gè)帶有時(shí)間戳的對(duì)象引用 AtomicStampedReference,其內(nèi)部不僅維護(hù)了一個(gè)對(duì)象值,還維護(hù)了一個(gè)時(shí)間戳,當(dāng)AtomicStampedReference對(duì)應(yīng)的數(shù)值被修改時(shí),除了更新數(shù)據(jù)本身,還需要更新時(shí)間戳,只有對(duì)象值和時(shí)間戳都滿足期望值,才能修改成功。這是AtomicStampedReference的幾個(gè)有關(guān)時(shí)間戳信息的方法:

//比較設(shè)置 參數(shù)依次為:期望值 寫入新值 期望時(shí)間戳 新時(shí)間戳
public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp)
//獲得當(dāng)前時(shí)間戳
public int getStamp()
//設(shè)置當(dāng)前對(duì)象引用和時(shí)間戳
public void set(V newReference, int newStamp)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容