淺析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…

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM