Java之CAS無鎖算法

 若是一個線程失敗或掛起不會致使其餘線程也失敗或掛起,那麼這種算法就被稱爲非阻塞算法。而CAS就是一種非阻塞算法實現,也是一種樂觀鎖技術。它能在不使用鎖的狀況下實現多線程之間的變量同步,因此CAS也是一種無鎖算法。html

一、CAS的實現

 CAS包含了3個操做數——須要讀寫的內存位置V、進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS纔會經過原子方式用新值B來更新V的值,不然不會寫入新值,下面來看模擬CAS的實現。java

//CAS模擬實現
    public class SimulatedCAS {
        private int value;
    
        public synchronized int get() {
            return value;
        }
    
        public synchronized int compareAndSwap(int expectedValue, int newValue) {
            int oldValue = value;
            if (oldValue == expectedValue) {
                value = newValue;
            }
            return oldValue;
        }
    
        public synchronized boolean compareAndSet(int expectedValue, int newValue) {
            return (expectedValue == compareAndSwap(expectedValue, newValue));
        }
    }
複製代碼

SimulatedCAS就是模擬CAS的實現,由於這裏僅僅是模擬CAS,明白其語義,因此代碼仍是很簡單的。算法

//基於CAS實現的一個線程安全計數器
    public class CasCounter {
        private SimulatedCAS value;

        public int getValue() {
            return value.get();
        }

        public int increment() {
            int v;
            do {
                v = value.get();
            } while (v != value.compareAndSwap(v, v + 1));
            return v + 1;
        }
    }
複製代碼

CasCounter就是基於SimulatedCAS實現的一個線程安全的計數器,CasCounter不會阻塞,但若是其餘線程同時更新更新計數器,那麼將會屢次執行重試操做。理論上,若是其餘線程在每次競爭CAS時老是獲勝,那麼這個線程每次都會重試,但實際上不多發生這種類型的飢餓問題。編程

二、原子變量類

 Java5.0引入了底層的支持,在int、long和對象引用等類型上也都公開了CAS操做,而且JVM把它們編譯爲底層硬件提供的最有效方法。在支持CAS的平臺,運行時把它們編譯爲相應的(多條)機器指令。最壞的狀況下,若是不支持CAS指令,那麼JVM將使用自旋鎖。所以Java提供的CAS解決方案就與平臺/編譯器緊密相關,其性能也受平臺/編譯器影響。數組

 原子變量類(例如java.util.concurrent.atomic中的AtomicXxx)就使用了這種高效的CAS操做。安全

類型 具體實現類 描述
基本數據類型 AtomicInteger 原子更新Integer類型
AtomicBoolean 原子更新boolean類型
AtomicLong 原子更新long類型
數組類型 AtomicIntegerArray 原子更新Integer數組類型
AtomicLongArray 原子更新long數組類型
AtomicReferenceArray 原子更新對象數組類型
引用類型 AtomicReference 原子更新對象
AtomicStampedReference 原子更新對象,解決ABA問題
AtomicMarkableReference 原子更新對象,解決ABA問題
更新字段類型 AtomicIntegerFieldUpdater 原子更新已存在的volatile修飾的Integer類型,使用反射實現
AtomicLongFieldUpdater 原子更新已存在的volatile修飾的long類型,使用反射實現
AtomicReferenceFieldUpdater 原子更新已存在的volatile修飾的對象,使用反射實現

 上面就是Java中的全部原子變量類,基本上都比較好理解,可是個別的仍是須要說明一下。多線程

AtomicStampedReferenceAtomicMarkableReference都是爲了解決ABA問題而存在的。但它們是有區別的,AtomicStampedReference是給對象引用上加一個版本號來避免ABA問題,而AtomicMarkableReference是給對象引用加上一個標記(boolean類型)來避免ABA問題的。併發

 更新字段類型又叫原子的域更新器,表示現有volatile的一種基於反射的「視圖」,從而可以在已有的volatile域上使用CAS。它提供的原子性保證比普通原子類更弱一些,由於沒法直接保證底層的域不被直接修改——compareAndSet以及其餘算術方法只能確保其餘使用更新器方法的線程的原子性。app

 下面來看AtomicInteger的源碼。工具

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            //valueOffset能夠說是value在JVM中的虛擬內存地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    //傳入初始化值
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    //無參
    public AtomicInteger() {
    }

    //返回當前值
    public final int get() {
        return value;
    }
    //設置新值
    public final void set(int newValue) {
        value = newValue;
    }

    //1.6新增方法,設置新值,但不會當即刷新,性能比set方法好
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    //設定新值並返回舊值
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    //比較並替換,Java CAS實現方法
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    //比較並替換,Java CAS實現方案,與compareAndSet實現同樣名單不排除將來會有不一樣實現
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    //以原子方式將當前值增長1。返回舊值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    //以原子方式將當前值減1。返回舊值
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

    //以原子方式將給定值添加到當前值。返回舊值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //以原子方式將當前值增長1。返回新值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    //以原子方式將當前值減1。返回新值
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    //以原子方式將給定值添加到當前值。返回新值
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    //1.8新增方法,更新並返回舊值
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    //1.8新增方法,更新並返回新值
    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

    //1.8新增方法,更新並返回舊值
    public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    //1.8新增方法,更新並返回新值
    public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }
    //都是些類型轉換的實現
    ...
}
複製代碼

valueOffset是變量在JVM中的內存地址,也就是CAS中所說的內存位置V。setlazySetcompareAndSetweakCompareAndSet這兩組方法須要注意一下。

 因爲valuevolatile修飾的,因此當調用set方法時,全部線程的value值會當即更新,而lazySet則不會,只有在從新寫入新值的時候纔會更新,而讀取的仍是舊值。關於這兩個方法的區別能夠參考J.U.C原子工具類AtomicXXX中,set和lazySet的區別AtomicInteger lazySet vs. set這兩篇文章。

compareAndSetweakCompareAndSet這兩個方法就比較有意思,由於實現如出一轍的,那是爲何尼?在Stack Overflow上說,Java在將來可能會有不一樣的實現,關於這兩個方法的區別能夠參考java – 若是weakCompareAndSet若是實現徹底像compareAndSet,會如何失敗?這篇文章。

 在AtomicInteger及其餘原子變量類中,都是使用Unsafe來實現CAS的,該類大部分都是native方法且不對外公開,由於是不安全的。該類與硬件相關,都是CPU指令級的操做,只有一步原子操做,並且CAS避免了請求操做系統來裁定鎖的問題,不用麻煩操做系統,直接在CPU內部就搞定了,因此效率很是高且線程安全。

三、CAS原子操做的三大問題

 CAS主要有如下問題。

  • ABA問題。
  • 因爲CAS是CPU指令級別的操做,因此當循環時間過長容易形成CPU高負荷。
  • 對一個變量進行操做能夠,同時操做多個共享變量有點麻煩。能夠經過AtomicReference來解決。

 ABA問題是CAS中的一個異常現象,來回顧一下CAS的語義,當且僅當V的值等於A時,CAS纔會經過原子方式用新值B來更新V的值,不然不會執行任何操做。那麼若是先將V的值改成C而後再改成A,那麼其餘線程的CAS操做仍然可以成功,但這樣對嗎?很明顯是不對的,由於V的值變化了。那麼如何解決尼?其實能夠在變量前面添加版本號,每次變量更新的時候都把版本號加一,這樣變化過程就從「A-B-A」變成了「1A-2B-3A」。

四、總結

 在Java中,CAS的運用仍是比較普遍的,如對象建立的原子性、Java 1.8中ConcurrentHashMap及RxJava的多線程變量同步都是經過CAS來實現的。

【參考資料】

不可不說的Java「鎖」事

JAVA CAS原理深度分析

非阻塞同步算法與CAS(Compare and Swap)無鎖算法

【死磕Java併發】—-深刻分析CAS

Java併發編程-無鎖CAS與Unsafe類及其併發包Atomic

Psychosomatic, Lobotomy, Saw

相關文章
相關標籤/搜索