java樂觀鎖之CAS原理解析

含義

CAS(CompareAndSwap) 即比較並替換,實現併發算法時常用到的一種技術。CAS操作包含三個操作數——內存位置、預期原值及新值。執行CAS操作的時候,將內存位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新爲新值,否則,處理器不做任何操作。

樂觀鎖的含義就是假設沒有發生衝突,那麼我正好可以進行某項操作,如果要是發生衝突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS。
無論是ReenterLock內部的AQS,還是各種Atomic開頭的原子類,內部都應用到了CAS

原理分析

以java.util.concurrent.atomic.***AtomicInteger***爲例進行分析

/**
 * An {@code int} value that may be updated atomically.  See the
 * {@link java.util.concurrent.atomic} package specification for
 * description of the properties of atomic variables. An
 * {@code AtomicInteger} is used in applications such as atomically
 * incremented counters, and cannot be used as a replacement for an
 * {@link java.lang.Integer}. However, this class does extend
 * {@code Number} to allow uniform access by tools and utilities that
 * deal with numerically-based classes.
 *
 * @since 1.5
 * @author Doug Lea
*/
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 = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
     /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    ...
    }
  • Unsafe是位於sun.misc包下的一個類,Unsafe類使Java語言擁有了類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語言變得不再“安全”,因此對Unsafe的使用一定要慎重。

在jdk1.9中,對Usafe進行了刪除,因此那些基於Usafe開發的框架慢慢的都死掉了。

  • valueOffset是long類型的,此處代表的含義就是對象成員變量value相對對象內存地址的偏移量

valueOffset的賦值是放在static代碼塊中,也就是說類加載初始化的時候就執行一次,從而獲取了value成員變量相對於當前對象內存地址的偏移量

  • value變量使用volatile修飾保證了內存可見性,爲後面的CAS提供了可能性。其實實際存儲的值是放在value中的
  • incrementAndGet方法剖析
    調用鏈:
    incrementAndGet() → unsafe.getAndAddInt(this, valueOffset, 1) + 1 → getAndAddInt(Object var1, long var2, int var4)
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;
    }

var1: 對應當前對象this; var2對應偏移量valueOffset;var4對應增量 1;
所以上面代碼等價於

public final int getAndAddInt(Object this,long valueOffset,int 1){
    int var5;
    do {
        var5 = this.getIntVolatile(this, valueOffset);
    } while(!this.compareAndSwapInt(this, valueOffset, var5, var5 + 1));
    return var5;
    }

通過上面的變量替換,只剩下var5了。

var5 = this.getIntVolatile(this, valueOffset);

這是個native方法,其實就是獲取var1中,var2偏移量處的值。var1就是AtomicInteger,var2就是我們前面提到的valueOffset,這樣我們就從內存裏獲取到現在valueOffset處的值了,也就是獲取到當前value的值

public final int getAndAddInt(Object this,long valueOffset,int 1){
    int expect;
    do {
        expect = this.getIntVolatile(this, valueOffset);
    } while(!this.compareAndSwapInt(this, valueOffset, expect, update));
    return expect;
    }

getAndAddInt方法執行邏輯:

  1. 通過getIntVolatile(this,valueOffset)獲取到當前value的值,作爲expect(期望值)。
  2. compareAndSwapInt方法含義:
    如果this內的value和expect相等,就證明沒有其他線程改變過這個變量,那麼就更新它爲update,如果這一步的CAS沒有成功,那就採用自旋的方式(無限循環)繼續進行CAS操作

CPU原語

CAS,比較並交換。乍一看這也是兩個步驟了啊,其實在JNI裏是藉助於一個CPU指令(cmpxchgl)完成的。所以還是原子操作。

CAS缺陷

ABA問題

  • 問題描述

CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。這就是CAS的ABA問題。

  • 解決辦法

常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
目前在JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

  • AtomicStampedReference原理解析

AtomicStampedReference就是加了版本號的AtomicReference。
可以看到,除了傳入一個初始的引用變量initialRef外,還有一個initialStamp變量,initialStamp其實就是版本號(或者說時間戳),用來唯一標識引用變量。

源碼如下:

/**
 * An {@code AtomicStampedReference} maintains an object reference
 * along with an integer "stamp", that can be updated atomically.
 *
 * <p>Implementation note: This implementation maintains stamped
 * references by creating internal objects representing "boxed"
 * [reference, integer] pairs.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <V> The type of object referred to by this reference
 */
public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference; //變量引用
        final int stamp; //版本號或者叫時間戳,一般做自增就可以
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

    /**
     * Creates a new {@code AtomicStampedReference} with the given
     * initial values.
     *
     * @param initialRef the initial reference
     * @param initialStamp the initial stamp
     */
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }

    /**
     * Returns the current value of the reference.
     *
     * @return the current value of the reference
     */
    public V getReference() {
        return pair.reference;
    }

    /**
     * Returns the current value of the stamp.
     *
     * @return the current value of the stamp
     */
    public int getStamp() {
        return pair.stamp;
    }
    ...     
    /**
     * 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
     * @param newStamp the new value for the stamp
     * @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 &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    ... 

    // Unsafe mechanics

    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
    
    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
}
  • 實例:
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(new Integer(100), 1);
        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                boolean flag = false;
                do {
                    int stamp = atomicStampedReference.getStamp();
                    flag = atomicStampedReference.compareAndSet((Integer) atomicStampedReference.getReference(), Integer.valueOf((Integer) ((Integer) atomicStampedReference.getReference()).intValue() + 1), stamp, stamp + 1);
                } while (!flag);
            });
            list.add(thread);
            thread.start();
        }
        list.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println((Integer) atomicStampedReference.getReference()); // 1100

循環時間長開銷大

上面我們說過如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。

只能對單個共享變量保證原子性操作

這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行CAS操作。

  • AtomicReference原理分析
    AtomicReference就是以原子方式更新對象引用。
    源碼如下
/**
 * An object reference that may be updated atomically. See the {@link
 * java.util.concurrent.atomic} package specification for description
 * of the properties of atomic variables.
 * @since 1.5
 * @author Doug Lea
 * @param <V> The type of object referred to by this reference
 */
public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;

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

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile V value;

    /**
     * Creates a new AtomicReference with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicReference(V initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicReference with null initial value.
     */
    public AtomicReference() {
    }
    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }
   /**
     * Atomically updates the current value with the results of
     * applying the given function, returning the updated value. The
     * function should be side-effect-free, since it may be re-applied
     * when attempted updates fail due to contention among threads.
     *
     * @param updateFunction a side-effect-free function
     * @return the updated value
     * @since 1.8
     */  
 public final V updateAndGet(UnaryOperator<V> updateFunction) {
        V prev, next;
        do {
            prev = get();
            next = updateFunction.apply(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }
    ...
    }

通過上述源碼可以看到,AtomicReference使用泛型定義。操作的是對象引用,而不是具體某個變量(AtomicInteger等),從而可以操作共享資源對象,而不單單是共享變量。(具體原理可以參考上面AtomicInteger的原理解析,大同小異,只是把int類型的value換成了對象引用V)

  • 實例
AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000));
        List<Thread> list = new ArrayList<>();
        UnaryOperator<Integer> integerUnaryOperator = x -> x + 1;
        for(int i=0;i<1000;i++){
             Thread thread = new Thread( ()->{ref.updateAndGet(integerUnaryOperator);});
            list.add(thread);
            thread.start();
        }
        list.forEach( t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(ref.get()); // 2000
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章