java學習記錄---CAS樂觀鎖

CAS,全稱 Compare And Swap (比較與交換),是一種樂觀鎖,一樣是鎖相比 synchronized 性能卻要高很多,由於是 synchronized 阻塞的,而CAS是非阻塞的。CAS主要有3個操做數,內存值V,預期的舊值A,須要修改的新值B,能夠這樣理解:在V的地址把A改爲B,當V位置的值與預期的舊值A相同時候,則修改爲B,不然不更新。java

下面看個圖簡單理解一下CAS:當線程1和線程2同時操做內存V,線程1想要把內存V的變量值從A(2)改爲B(1)而線程2想要把V的變量值從A(2)改爲B(3)。假設這個時候是線程1優先搶到資源因此線程1先進行CAS操做,這個時候預期舊值2是相等的則執行了更新,更新完後內存V的變量值就變成1,這個時候線程2才進入比較預期的A值與V中實際的變量值已經不相同了,因此更新失敗。ide

這個圖看上去是Compare And Swap 是同時操做,但其實是分2部執行:1.比較(compare),2.交換(swap),它的原子性是經過硬件實現的,而不是咱們java代碼實現函數

java提供的CAS操做類性能

咱們隨便找其中一個Atomic類學習學習

當V的值與A相等則更新成功測試

public static void main(String[] args) {
    // 定義IntegerCAS    AtomicInteger AI = new AtomicInteger();
    // 設置初始化值
    AI.set(1);
    //  1 替換成 2 並返回是否替換成功,該函數第一個參數爲預期的舊值(A),第二個參數是須要修改的值(B)
    boolean b = AI.compareAndSet(1, 2);
    // 打印是否替換成功
    System.out.println(b);
    // 打印最新值
    System.out.println(AI);
}

打印結果:this

當V的值與A不相等則更新失敗spa

public static void main(String[] args) {
    // 定義IntegerCAS    AtomicInteger AI = new AtomicInteger();
    // 設置初始化值
    AI.set(1);
    //  1 替換成 2 並返回是否替換成功,該函數第一個參數爲預期的舊值(A),第二個參數是須要修改的值(B)
    boolean b = AI.compareAndSet(2, 2);
    // 打印是否替換成功
    System.out.println(b);
    // 打印最新值
    System.out.println(AI);
}

打印結果:線程

這2個打印結果的結論也證明了最開始那個圖的原理,只有V的變量值與A相同時候,纔會修改爲B ,3d

接下來看看原子性

普通的int類型

public class AmIntegerTest implements Runnable {

    private static volatile int I = 0;

    public void run() {
        // 每一個線程自增100000次,
        for (int i = 0; i++ < 100000; add()) {}
        // 線程執行完以後結果
        System.out.println(Thread.currentThread().getName() + ":" + I);
    }

    public static void add(){
        I++;
    }

    public static void main(String[] args) {
        AmIntegerTest ait = new AmIntegerTest();
        Thread t1 = new Thread(ait);
        Thread t2 = new Thread(ait);
        t1.start();
        t2.start();
    }
}

打印結果顯示,普通的int沒有原子性

除非加上synchronized 關鍵字,接下來改造add()添加synchronized其餘代碼不變

public synchronized static void add(){
    I++;
}

不管執行多少次最終結果都是200000,能夠保證原子性

 

咱們再看看java提供的CAS操做類

public class AmIntegerTest implements Runnable {

    // 定義IntegerCAS    private static AtomicInteger AI = new AtomicInteger();

    public void run() {
        // 每一個線程自增100000次,
        for (int i = 0;i++ < 100000; AI.incrementAndGet()) {}
        // 線程執行完以後結果
        System.out.println(Thread.currentThread().getName() + ":" + AI.get());
    }

    public static void main(String[] args) {
        AmIntegerTest ait = new AmIntegerTest();
        Thread t1 = new Thread(ait);
        Thread t2 = new Thread(ait);
        t1.start();
        t2.start();
    }
}

不管執行多少次最終結果都是200000,因此是具備原子性的,可是它的原子性是其餘語言實現的,這裏就不討論它的實現原理了

AtomicInteger.value是一個volatile 修飾的變量(內存鎖定,同一時刻只有一個線程能夠修改內存值)

private volatile int value;

AtomicInteger.incrementAndGet()這是一個自增函數,實現了自旋鎖(無限循環),下面看看incrementAndGet()的源代碼,它調用了Unsafe 類的getAndAddInt()。而getAndAddInt()是無限循環,直到值修改爲功才結束,不然一直循環

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
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;
}

getAndAddInt()實現的自旋鎖原理就是內存V的變量值與A不一致時候,再從新獲取V的變量值,直到V的變量值與A一致時候,才更新成B並結束,這樣有個缺點就是若是自旋次數太多,會形成很大的資源消耗

在Atomic包中的CAS操做都是基於如下3個方法實現,Unsafe類裏面的全部方法都是 native 聲明的,說明是調用其餘語言實現的

//第一個參數o爲給定對象,offset爲對象內存的偏移量,經過這個偏移量迅速定位字段並設置或獲取該字段的值,
//expected表示指望值,x表示要設置的值,下面3個方法都經過CAS原子指令執行操做。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

咱們試試使用一下這些函數

public class CasTest {

    // 定義java CASUnsafe
    public static Unsafe U = getUnsafe();

    public static void main(String[] args) throws NoSuchFieldException {
        // java Unsafe類調用C++ 實現的CAS
        // 建立一個咱們的測試對象
        Cas cas = new Cas("1");
        // 獲取對象內value存儲對象的地址
        long offset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));
        // 經過Unsafe類的CAS函數進行修改
        System.out.println(U.compareAndSwapObject(cas, offset, "1", "2"));
        System.out.println(cas);
    }

    static class Cas{
        private String value;

        public Cas(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return "Cas{" +
                    "value='" + value + '\'' +
                    '}';
        }
    }

    // 由於 Unsafe的構造函數是私有的,並且它提供的 getUnsafe()也只有系統類才能使用,
    // 因此咱們只能經過反射獲取Unsafe實例了
    private static Unsafe getUnsafe() {
        Unsafe unsafe = null;
        try {
            Constructor<?> declaredConstructor = Unsafe.class.getDeclaredConstructors()[0];
            declaredConstructor.setAccessible(true);
            unsafe = (Unsafe) declaredConstructor.newInstance();
        } catch (Exception e) { e.printStackTrace(); }
        return unsafe;
    }

}

看一下main()的打印結果,修改cas對象的value值,從「1」改爲「2」,執行成功

改一下main(),試一下從「2」改爲「2」

public static void main(String[] args) throws NoSuchFieldException {
    // java Unsafe類調用C++ 實現的CAS
    // 建立一個咱們的測試對象
    Cas cas = new Cas("1");
    // 獲取對象內value存儲對象的地址
    long offset = U.objectFieldOffset(cas.getClass().getDeclaredField("value"));
    // 經過Unsafe類的CAS函數進行修改
    System.out.println(U.compareAndSwapObject(cas, offset, "2", "2"));
    System.out.println(cas);
}

這個時候修改是失敗的,由於value的值爲「1」,而CAS操做裏 預期的舊值是「2」,因此沒法成功執行從「2」改爲「2」

下面2個函數都是Unsafe類裏面的,都是私有的,並且它提供的 getUnsafe()也不能使用因此須要經過反射獲取實例

private Unsafe() {
}

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

ABA問題

ABA就是內存V的變量值從A變成B,再從B變成A,這個時候CAS只判斷值是否相等,只要值相等就會認爲這個值沒改變過,但其實是已經變化了。咱們能夠添加多一個標識(版本號、時間)判斷這個值是否已經改變過,java也提供了相關的解決方案 AtomicStampedReference 類。

先看一下普通CAS的操做類

public static void main(String[] args) {
    // 定義IntegerCAS    AtomicInteger AI = new AtomicInteger(1);
    System.out.println("初始化值:" + AI);
    AI.compareAndSet(1,2);
    System.out.println("第一次CAS操做完成以後值:" + AI);
    AI.compareAndSet(2,1);
    System.out.println("第二次CAS操做完成以後值:" + AI);
    AI.compareAndSet(1,3);
    System.out.println("第二次CAS操做完成以後值:" + AI);
}

這種只須要V的值與A一致就能夠修改,存在ABA問題

接下來看看CAS的標識引用類

public static void main(String[] args) {
    // 定義 AtomicStampedReference 類,第一個參數是咱們的初始化值,第二個是版本標識
    AtomicStampedReference<Integer> ASR = new AtomicStampedReference<Integer>(1,1);
    System.out.println("初始化值:" + ASR.getReference());
    ASR.compareAndSet(1,2, ASR.getStamp(), ASR.getStamp() + 1);
    System.out.println("第一次CAS操做完成以後值:" + ASR.getReference());
    // 獲取stamp ,下面2次新增都使用這個 stamp    int stamp = ASR.getStamp();
    // 這一次能夠修改爲功,由於 stamp 的預期值一致
    ASR.compareAndSet(2,1, stamp, stamp + 1);
    System.out.println("第二次CAS操做完成以後值:" + ASR.getReference());
    // 這一次能夠修改失敗,由於 stamp 的預期值不一致
    ASR.compareAndSet(1,3, stamp, stamp + 1);
    System.out.println("第三次CAS操做完成以後值:" + ASR.getReference());
}

這種不緊V的值要與A相等,且記錄的標識也須要一致才能完成修改

CAS是樂觀鎖,synchronized是悲觀鎖。二者各有優異,應對不一樣場景選擇合適的鎖。

相關文章
相關標籤/搜索