Java併發編程:什麼是CAS?這回總算知道了

無鎖的思想

衆所周知,Java中對併發控制的最常見方法就是鎖,鎖能保證同一時刻只能有一個線程訪問臨界區的資源,從而實現線程安全。然而,鎖雖然有效,但採用的是一種悲觀的策略。它假設每一次對臨界區資源的訪問都會發生衝突,當有一個線程訪問資源,其他線程就必須等待,所以鎖是會阻塞線程執行的。

當然,凡事都有兩面,有悲觀就會有樂觀。而無鎖就是一種樂觀的策略,它假設線程對資源的訪問是沒有衝突的,同時所有的線程執行都不需要等待,可以持續執行。如果遇到衝突的話,就使用一種叫做CAS (比較交換) 的技術來鑑別線程衝突,如果檢測到衝突發生,就重試當前操作到沒有衝突爲止。

CAS概述

CAS的全稱是 Compare-and-Swap,也就是比較並交換,是併發編程中一種常用的算法。它包含了三個參數:V,A,B。

其中,V表示要讀寫的內存位置,A表示舊的預期值,B表示新值

CAS指令執行時,當且僅當V的值等於預期值A時,纔會將V的值設爲B,如果V和A不同,說明可能是其他線程做了更新,那麼當前線程就什麼都不做,最後,CAS返回的是V的真實值。

而在多線程的情況下,當多個線程同時使用CAS操作一個變量時,只有一個會成功並更新值,其餘線程均會失敗,但失敗的線程不會被掛起,而是不斷的再次循環重試。正是基於這樣的原理,CAS即時沒有使用鎖,也能發現其他線程對當前線程的干擾,從而進行及時的處理。

CAS的應用類

Java中提供了一系列應用CAS操作的類,這些類位於java.util.concurrent.atomic包下,其中最常用的就是AtomicInteger,該類可以看做是實現了CAS操作的Integer,所以,下面我們就通過學習該類的案例來一窺全貌CAS的妙用。

學習AtomicInteger之前,我們先來看一段代碼實例:

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函數中開啓了10個線程,執行後會輪流調用 increase(),當然我們知道,運行後輸出的結果肯定不是我們期望的值,因爲沒有做線程安全的處理,所以10個線程流量操作臨界區的資源NUMBER就會出錯。

解決辦法並不難,用我們之前學過的鎖,例如synchronized修飾代碼塊,程序就會正常輸出10000。當然,用鎖解決並不是我們想要的方式,因爲鎖會阻塞線程,影響程序的性能,這時候,AtomicInteger就可以派上用場了。

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

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);
}

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

上面的代碼中,increase方法裏調用了NUMBER.getAndIncrement() ,這是AtomicInteger的自增方法,會對當前的值加1,並且返回舊值,點進方法的源碼,它調用的是unsafe.getAndAddInt()方法:

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

getAndAddInt的作用是對當前值加1,並返回舊值。

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

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

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

這裏要擴展一個知識點,就是Java本身無法訪問操作系統,需要使用native方法,而Unsafe類中的方法就包含了大量的native方法,提高了Java對系統底層的原子操作能力。例如我們代碼中使用到的getAndAddInt()底層就是調用一個native方法,用idea點擊方法,得到下面反編譯後的代碼:

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的作用是比較並交換整數值,如果指定的字段的值等於期望值,也就是CAS中的 'A' (預期值),那麼就會把它設置爲新值 (CAS中的 'B'),不難想象,該方法內部的實現必然是依靠原子操作完成的。除此之外,Unsafe類中還提供了其他的原子操作的方法,例如上面源碼中的getIntVolatile就是使用volatile語義獲得給定對象的值,這些方法通過底層的原子操作高效的提升了應用層面的性能。

CAS的缺點

雖然CAS的性能比起鎖要強大很多,但它也存在一些缺點,例如:

1、循環的時間開銷大

在getAndAddInt的方法中,我們可以看到,只是簡單的設置一個值卻調用了循環,如果CAS失敗,會一直進行嘗試。如果CAS長時間不成功,那麼循環就會不停的跑,無疑會給系統造成很大的開銷。

2、ABA問題

前面說過,CAS判斷變量操作成功的條件是V的值和A是一致的,這個邏輯有個小小的缺陷,就是如果V的值一開始爲A,在準備修改爲新值前的期間曾經被改成了B,後來又被改回爲A,經過兩次的線程修改對象的值還是舊值,那麼CAS操作就會誤任務該變量從來沒被修改過。這就是CAS中的“ABA”問題。

當然,"ABA"問題也有解決方案,Java併發包中提供了一個帶有時間戳的對象引用 AtomicStampedReference,其內部不僅維護了一個對象值,還維護了一個時間戳,當AtomicStampedReference對應的數值被修改時,除了更新數據本身,還需要更新時間戳,只有對象值和時間戳都滿足期望值,才能修改成功。這是AtomicStampedReference的幾個有關時間戳信息的方法:

//比較設置 參數依次爲:期望值 寫入新值 期望時間戳 新時間戳
public boolean compareAndSet(V expectedReference, V newReference,
                             int expectedStamp, int newStamp)
//獲得當前時間戳
public int getStamp()
//設置當前對象引用和時間戳
public void set(V newReference, int newStamp)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章