1. 程式人生 > >java併發CAS

java併發CAS

CAS你知道嗎?

併發程式中,對於count++這種操作,如果採用鎖的方式,需要先獲取到鎖,最後要釋放掉鎖,獲取不到需要等待,還需要被喚醒,這就產生了鎖競爭,和執行緒之間頻繁排程帶來的系統開銷。對於這種情況完全可以採用原子變數AtomicInteger代替,AtomicInteger底層就是使用的CAS操作。

CAS的思想是什麼?

CAS操作包含三個運算元—— 記憶體位置的值(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置更新為新值。

CAS是一條CPU的原子指令,不會造成所謂的資料不一致問題。在java 中主要是sun.misc.Unsafe這個類裡面的若干方法。

 

下面的的程式count++使用cas的方式保證執行緒安全,最後count的值總是150.

public class AtomicIntegerDemo {
    static AtomicInteger count = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 50; i++) {
                        try {
                            Thread.sleep(20); //休眠20毫秒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        int value = count.incrementAndGet();//++操作
                        System.out.println(Thread.currentThread().getName() + ":" + value);
                    }
                }
            }, "執行緒" + i).start();
        }

        Thread.sleep(Integer.MAX_VALUE);
    }
}

 

count.incrementAndGet()分析:這裡呼叫了unsafe.getAndAddInt(this, valueOffset, 1)方法。傳入了三個引數:

1、當前AtomicInteger物件

2、當前物件value變數的記憶體地址:通過unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"))獲取

    private static final long valueOffset;

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

3、加多少,這裡要加1

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

getAndAddInt方法:這裡呼叫了compareAndSwapInt方法,

引數1:物件記憶體位置

引數2:物件中變數的偏移量

引數3:變數預期值

引數4:新的值

如果compareAndSwapInt 操作失敗,重新獲取期望值,繼續迴圈,直到更新成功。

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

併發包下面和AtomicInteger類似的還有

1、AtomicLong:原子Long型別

2、AtomicBoolean:原子Boolean型別

3、AtomicReference:普通物件的原子引用

 

下面的程式使用AtomicReference包裝了String型別,呼叫compareAndSet方法去更新atomicStr 的值為hello,只有一個執行緒可以更新成功。其他都會失敗,因為預期值和記憶體裡的值不一樣。

public class AtomicReferenceTest {
    public final static AtomicReference<String> atomicStr = new AtomicReference<String>("hello");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            final int num = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(Math.abs((int) (Math.random() * 100)));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (atomicStr.compareAndSet("hello", "nihao")) {
                        System.out.println(Thread.currentThread().getName() + " success: " + atomicStr);
                    } else {
                        System.out.println(Thread.currentThread().getName() + " failure: " + atomicStr);
                    }
                }
            }, "執行緒" + i).start();
        }

        Thread.sleep(Integer.MAX_VALUE);
    }
}

CAS引出來的ABA問題?

比如count值為0,執行緒1將count的值修改成1,執行緒2把count又修改成了0,執行緒3去更新的時候,預期值和記憶體裡的值都是0,即使count已經被修改過了,依然可以更新成功。

這時候就需要使用AtomicStampedReference,它增加了時間戳。

引數1:初始值

引數2:初始時間戳

static AtomicStampedReference<Integer> count = new AtomicStampedReference<Integer>(0, 0);

獲取時間戳

int timestamp = count.getStamp();

引數1:期望的物件引用

引數2:更新的物件引用

引數3:期望的時間戳

引數4:更新的時間戳

 

除了ABA問題,cas還存在哪些問題?
1、迴圈時間太長
在Java程式碼示例中我們可以看到用的是無限迴圈。如果自旋CAS長時間不成功,為給CPU帶來很大的開銷。

 

2、保證單個共享變數是原子操作
通過程式碼示例也可以看出,CAS只能針對單個變數;如果是多個變數那麼就要使用鎖了。

AtomicReference類來保證引用物件之間的原子性,把多個變數放在一個物件裡來進行CAS操作,倒是可以解決這個問題。