CAS筆記

CAS(Compare And Swap)

獨佔鎖是一種悲觀鎖,而 synchronized 就是一種獨佔鎖,synchronized 會導致其它所有未持有鎖的線程阻塞,而等待持有鎖的線程釋放鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。而樂觀鎖用到的機制就是CAS。在java中可以通過鎖和循環CAS的方式來實現原子操作。java.util.concurrent.atomic包相關類就是CAS的實現。

CAS機制當中使用了3個基本操作數:內存地址V(value,volatile修飾的變量),舊的預期值A(expected),要修改的新值B(updated)。

更新一個變量的時候,只有當前變量的預期值cur和內存地址V中的實際值(value)相同時,纔會將內存地址V對應的值(value)修改爲B(updated)。

  • CAS機制實現自增(保證原子性)
/**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);//獲取當前值,相當與上面提到的預期值A
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  //比較當前值與實際值,var5+var4相當於updated

        return var5;  //返回swap後的值
}

getAndAddInt中的內容是:
1. 獲取當前值var5通過getIntVolatile()。
2. while循環,如果當前值var5 等於 實際值value,則compareAndSwapInt返回true,將實際值更新後,並跳出循環。若不等於,則進入循環,嘗試重新獲取當前值,將當前值重新與實際值compare,這個重新嘗試的過程被稱爲自旋
3. 返回交換後的值。

從思想上來說,Synchronized屬於悲觀鎖,悲觀地認爲程序中的併發情況嚴重,所以嚴防死守。CAS屬於樂觀鎖,樂觀地認爲程序中的併發情況不那麼嚴重,所以讓線程不斷去嘗試更新。

package cn.itcast.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger ai = new AtomicInteger();
    private int i = 0;

    /** 使用CAS實現線程安全計數器 */
    private void safeCount() {
        for (;;) {
            int i = ai.get();
            // 如果實際值(執行到這一步時,從內存V中獲取的value) == 預期值(上一步獲取的當前值),則以原子方式將該值設置爲給定的更新值
            boolean suc = ai.compareAndSet(i, ++i);  //第一個參數:expected value;第二個參數:updated value
            if (suc) {
                break;
            }
        }
    }

    /** 非線程安全計數器 */
    private void count() {
        i++;
    }

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<Thread>();
        // 添加1000個線程
        for (int j = 0; j < 1000; j++) {
            ts.add(new Thread(new Runnable() {
                public void run() {
                    // 執行1000次計算,預期結果應該是1000000
                    for (int i = 0; i < 1000; i++) {
                        cas.count();
                        cas.safeCount();
                    }
                }
            }));
        }
        //開始執行
        for (Thread t : ts) {
            t.start();
        }
        // 等待所有線程執行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /**
         * 非線程安全計數結果:999689
         * 線程安全計數結果:1000000
         */
        System.out.println("非線程安全計數結果:"+ cas.i);
        System.out.println("線程安全計數結果:"+ cas.ai.get());
    }
}

正是unsafe(提供硬件級別的原子操作)的compareAndSwapInt方法保證了Compare和Swap操作之間的原子性操作。(比較返回爲true才進行替換,否則不執行任何操作(自旋))

CAS的缺點
  1. 循環時間開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
  2. 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,可以使用循環CAS的方式來保證原子操作,但對於多個共享變量操作時,循環CAS就無法保證操作的原子性。這時候就可以用鎖;或者一個取巧的方式,就是把多個共享變量合併成一個對象來操作。從java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作。
  3. ABA問題:因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的 atomic包裏提供了一個類AtomicStampedReference 來解決ABA問題。這個類的 compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。(改用傳統的互斥同步可能會比原子類更高效)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章