併發編程-硬件加持的CAS操做夠快麼?

Talk is cheap

CAS(Compare And Swap),即比較並交換。是解決多線程並行狀況下使用鎖形成性能損耗的一種機制,CAS操做包含三個操做數——內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管位置V的值是否等於A, 都將返回V原有的值。java

CAS的含義是」我認爲V的值應該是A,若是是,那我將V的值更新爲B,不然不修改並告訴V的值實際是多少「數據結構

Show you my code

在單線程環境中分別使用無鎖,加鎖以及cas進行十組5億次累加運算,而後打印出平均耗時。多線程

/**
 * cas對比加鎖測試
 *
 * @author Jann Lee
 * @date 2019-11-21 0:12
 **/
public class CasTest {

    @Test
    public void test() {
        long times = 500_000_000;
        // 記錄耗時
        List<Long> elapsedTime4NoLock = new ArrayList<>(10);
        List<Long> elapsedTime4Synchronized = new ArrayList<>(10);
        List<Long> elapsedTime4ReentrantLock = new ArrayList<>(10);
        List<Long> elapsedTime4Cas = new ArrayList<>(10);

        // 進行10組試驗
        for (int j = 0; j < 10; j++) {
            // 無鎖
            long startTime = System.currentTimeMillis();
            for (long i = 0; i < times; i++) {
            }
            long endTime = System.currentTimeMillis();
            elapsedTime4NoLock.add(endTime - startTime);

            // synchronized 關鍵字(隱式鎖)
            startTime = endTime;
            for (long i = 0; i < times; ) {
                i = addWithSynchronized(i);
            }
            endTime = System.currentTimeMillis();
            elapsedTime4Synchronized.add(endTime - startTime);

            // ReentrantLock 顯式鎖
            startTime = endTime;
            ReentrantLock lock = new ReentrantLock();
            for (long i = 0; i < times; ) {
                i = addWithReentrantLock(i, lock);
            }
            endTime = System.currentTimeMillis();
            elapsedTime4ReentrantLock.add(endTime - startTime);

            // cas(AtomicLong底層是用cas實現)
            startTime = endTime;
            AtomicLong atomicLong = new AtomicLong();
            while (atomicLong.getAndIncrement() < times) {
            }
            endTime = System.currentTimeMillis();
            elapsedTime4Cas.add(endTime - startTime);
        }

        System.out.println("無鎖計算耗時: " + average(elapsedTime4NoLock) + "ms");
        System.out.println("synchronized計算耗時: " + average(elapsedTime4Synchronized) + "ms");
        System.out.println("ReentrantLock計算耗時: " + average(elapsedTime4ReentrantLock) + "ms");
        System.out.println("cas計算耗時: " + average(elapsedTime4Cas) + "ms");

    }

    /**
     * synchronized加鎖
     */
    private synchronized long addWithSynchronized(long i) {
        i = i + 1;
        return i;
    }

    /**
     * ReentrantLock加鎖
     */
    private long addWithReentrantLock(long i, Lock lock) {
        lock.lock();
        i = i + 1;
        lock.unlock();
        return i;
    }

    /**
     * 計算平均耗時
     */
    private double average(Collection<Long> collection) {
        return collection.stream().mapToLong(i -> i).average().orElse(0);
    }
}

從案例中咱們可能看出在單線程環境場景下cas的性能要高於鎖相關的操做。固然,在競爭比較激烈的狀況下性能可能會有所降低,由於要不斷的重試和回退或者放棄操做,這也是CAS的一個缺點所在,由於這些重試,回退等操做一般用開發者來實現。架構

CAS的實現並不是是簡單的代碼層面控制的,而是須要硬件的支持,所以在不一樣的體系架構之間執行的性能差別很大。可是一個很管用的經驗法則是:在大多數處理器上,在無競爭的鎖獲取和釋放的」快速代碼路徑「上的開銷,大約是CAS開銷的兩倍。併發

爲什麼CAS如此優秀

硬件加持,現代大多數處理器都從硬件層面經過一些列指令實現CompareAndSwap(比較並交換)同步原語,進而使操做系統和JVM能夠直接使用這些指令實現鎖和併發的數據結構。咱們能夠簡單認爲,CAS是將比較和交換合成是一個原子操做jvm

JVM對CAS的支持, 因爲Java程序運行在JVM上,因此應對不一樣的硬件體系架構的處理則須要JVM來實現。在不支持CAS操做的硬件上,jvm將使用自旋鎖來實現。性能

CAS的ABA問題

cas操做讓咱們減小了鎖帶來的性能損耗,同時也給咱們帶來了新的麻煩-ABA問題。測試

在線程A讀取到x的值與執行CAS操做期間,線程B對x執行了兩次修改,x的值從100變成200,而後再從200變回100;然後在線程A執行CAS操做過程當中並未發現x發生過變化,成功修改了x的值。因爲x的值100 ->200->100,因此稱之爲ABA的緣由。atom

魔高一尺道高一丈,解決ABA的問題目前最經常使用的辦法就是給數據加上「版本號」,每次修改數據時同時改變版本號便可。
操作系統

Q&A

在競爭比較激烈的狀況下,CAS要進行回退,重試等操做才能獲得正確的結果,那麼CAS必定比加鎖性能要高嗎?

相關文章
相關標籤/搜索