1. 程式人生 > >淺析CAS與AtomicInteger原子類

淺析CAS與AtomicInteger原子類

一:CAS簡介

CAS:Compare And Swap(字面意思是比較與交換),JUC包中大量使用到了CAS,比如我們的atomic包下的原子類就是基於CAS來實現。區別於悲觀鎖synchronized,CAS是樂觀鎖的一種實現,在某些場合使用它可以提高我們的併發效能。

在CAS中,主要是涉及到三個運算元,所期盼的舊值、當前工作記憶體中的值、要更新的值,僅當所期盼的舊值等於當前值時,才會去更新新值。

二:CAS舉例

比如當如下場景,由於i++是個複合操作,讀取、自增、賦值三步操作,因此在多執行緒條件下我們需要保證i++操作的安全

public class CASTest {
    int i = 0;

    public void increment() {
        i++;
    }
}

解決辦法有通過使用synchronized來解決,synchronized解決了併發程式設計的原子性,可見性,有序性。

public class CASTest {
    int i = 0;

    public synchronized  void increment() {
        i++;
    }
}

但synchronized畢竟是悲觀鎖,儘管它後續進行了若干優化,引入了鎖的膨脹升級措施,但是還是存在膨脹為重量級鎖而導致阻塞問題,因此,我們可以使用基於CAS實現的原子類AtomicInteger來保證其原子性

public class CASTest {
    AtomicInteger i = new AtomicInteger(0);
    public  static void increment() {
        //自增並返回新值
        i.incrementAndGet();
    }
}

三:CAS原理分析

atomic包下的原子類就是基於CAS實現的,我們拿AtomicInteger來分析下CAS.

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

    // CAS操作是基於一個Unsafe類,Unsafe類是整個Concurrent包的基礎,裡面所有的函式都是native的
    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); }
    }
    //底層採用volatile修飾值,保證其可見性和有序性
    private volatile int value;

從AtomicInteger定義的相關屬性來看,其內部的操作都是基於Unsafe類,因為在Java中,我們並不能直接操作記憶體,但是Java還是開放了一個Unsafe類來給我們進行操作,顧名思義,Unsafe,是不安全的,因此要謹慎使用。

其內部定義的值是用volatiel進行修飾的,volatile可以保證有序性和可見性,具體為什麼可以保證就不在此闡述了。

再來看看其幾個核心的API

//以原子方式將值設定為給定的新值 expect:期望值 update:舊值
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//以原子方式將當前值+1,返回期望值
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

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

關於其原始碼還是很少的,基本都是基於Unsafe類進行實現的。

先來看看compareAndSet方法,其呼叫的是Unsafe的compareAndSwapInt方法,當工作記憶體中的值與所期盼的舊值不相同的時候,會更新失敗,舉例說明:

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        System.out.println("更新結果:"+atomicInteger.compareAndSet(2020, 2021));
        System.out.println("當前值為:"+atomicInteger.get());

        //自增加一
        atomicInteger.getAndIncrement();

        System.out.println("更新結果:"+atomicInteger.compareAndSet(2020, 2021));
        System.out.println("當前值為:"+atomicInteger.get());
    }
}

 

 在來看看incrementAndGet方法,其呼叫的是unsafe.getAndAddInt方法,其就相當於是自旋鎖的實現,當所期盼的舊值與新值相同時才更新成功,否則就進行自旋操作直到更新成功為止。

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

四:CAS缺點分析

CAS的優點很明顯,基於樂觀鎖的思想,提高了併發情況下的效能,缺點主要是ABA問題、自旋時間過長導致CPU佔有率過高、只能保證一個共享變數的原子性。

ABA問題

就是一個值由A變為B,在由B變為A,使用CAS操作無法感知到該種情況下出現的變化,帶來的後果很嚴重,比如銀行內部員工,從系統挪走一百萬,之後還了回來,系統感知不到豈不是要出事。模擬下出現ABA問題:
   public class ABA {
       private static AtomicInteger atomicInteger = new AtomicInteger(0);
   
       public static void main(String[] args) {
           //執行緒t1實現0->1->0
           Thread t1 = new Thread(new Runnable() {
               @Override
               public void run() {
                   atomicInteger.compareAndSet(0,1);
                   atomicInteger.compareAndSet(1,0);
               }
           },"t1");
   
           //執行緒t2實現0->100
           Thread t2 = new Thread(new Runnable() {
               @Override
               public void run() {
                   try {
                       //模擬狸貓換太子行為
                       TimeUnit.SECONDS.sleep(2);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println("更新結果:"+atomicInteger.compareAndSet(0, 100));
               }
           });
   
           t1.start();
           t2.start();
       }
   }
   

執行結果是:true

解決ABA可以使每一次修改都帶上時間戳,以記錄版本號的形式來使的CAS感知到這種狸貓換太子的操作。Java提供了AtomicStampedReference類來解決,該類除了指定舊值與期盼值,還要指定舊的版本號與期盼的版本號

    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)));
    }
public class ABA_Test {

    // 初始值100,版本號1
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1);

    public static void main(String[] args) throws InterruptedException {
        // AtomicStampedReference實現
        Thread tsf1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 讓 tsf2先獲取stamp,導致預期時間戳不一致
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 預期引用:100,更新後的引用:110,預期標識getStamp() 更新後的標識getStamp() + 1
                atomicStampedReference.compareAndSet(100, 110, atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp() + 1);
                atomicStampedReference.compareAndSet(110, 100, atomicStampedReference.getStamp(),
                        atomicStampedReference.getStamp() + 1);
            }
        });

        Thread tsf2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();

                try {
                    TimeUnit.SECONDS.sleep(2); // 執行緒tsf1執行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(
                        "AtomicStampedReference:" + atomicStampedReference.compareAndSet(100, 120, stamp, stamp + 1));
            }
        });

        tsf1.start();
        tsf2.start();
    }
}

執行結果:

自旋次數過長

 CAS是基於樂觀鎖的思想實現的,當頻繁出現當前值與所舊預期值不相等的情況,會導致頻繁的自旋而使得浪費CPU資源。

只能保證單個共享變數的原子性

單純對共享變數進行CAS操作,只能保證單個,無法使多個共享變數同時進行原子操作。

參考資料

狂神說Java:www.bilibili.com/video/BV1B7…
CAS機制及AtomicInteger原始碼分析:juejin.im/post/5e2182…