一篇搞定CAS,深度講解,面試實踐必備

背景

在高併發的業務場景下,線程安全問題是必須考慮的,在JDK5之前,可以通過synchronized或Lock來保證同步,從而達到線程安全的目的。但synchronized或Lock方案屬於互斥鎖的方案,比較重量級,加鎖、釋放鎖都會引起性能損耗問題。

而在某些場景下,我們是可以通過JUC提供的CAS機制實現無鎖的解決方案,或者說是它基於類似於樂觀鎖的方案,來達到非阻塞同步的方式保證線程安全。

CAS機制不僅是面試中會高頻出現的面試題,而且也是高併發實踐中必須掌握的知識點。如果你目前對CAS還不甚瞭解,或許只有模糊的印象,這篇文章一定值得你花時間學習一下。

什麼是CAS?

CASCompare And Swap的縮寫,直譯就是比較並交換。CAS是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令,這個指令會對內存中的共享數據做原子的讀寫操作。其作用是讓CPU比較內存中某個值是否和預期的值相同,如果相同則將這個值更新爲新值,不相同則不做更新。

本質上來講CAS是一種無鎖的解決方案,也是一種基於樂觀鎖的操作,可以保證在多線程併發中保障共享資源的原子性操作,相對於synchronized或Lock來說,是一種輕量級的實現方案。

Java中大量使用了CAS機制來實現多線程下數據更新的原子化操作,比如AtomicInteger、CurrentHashMap當中都有CAS的應用。但Java中並沒有直接實現CAS,CAS相關的實現是藉助C/C++調用CPU指令來實現的,效率很高,但Java代碼需通過JNI才能調用。比如,Unsafe類提供的CAS方法(如compareAndSwapXXX)底層實現即爲CPU指令cmpxchg。

CAS的基本流程

下面我們用一張圖來了解一下CAS操作的基本流程。

CAS操作流程圖

在上圖中涉及到三個值的比較和操作:修改之前獲取的(待修改)值A,業務邏輯計算的新值B,以及待修改值對應的內存位置的C。

整個處理流程中,假設內存中存在一個變量i,它在內存中對應的值是A(第一次讀取),此時經過業務處理之後,要把它更新成B,那麼在更新之前會再讀取一下i現在的值C,如果在業務處理的過程中i的值並沒有發生變化,也就是A和C相同,纔會把i更新(交換)爲新值B。如果A和C不相同,那說明在業務計算時,i的值發生了變化,則不更新(交換)成B。最後,CPU會將舊的數值返回。而上述的一系列操作由CPU指令來保證是原子的。

在《Java併發編程實踐》中對CAS進行了更加通俗的描述:我認爲原有的值應該是什麼,如果是,則將原有的值更新爲新值,否則不做修改,並告訴我原來的值是多少。

在上述路程中,我們可以很清晰的看到樂觀鎖的思路,而且這期間並沒有使用到鎖。因此,相對於synchronized等悲觀鎖的實現,效率要高非常多。

基於CAS的AtomicInteger使用

關於CAS的實現,最經典最常用的當屬AtomicInteger了,我們馬上就來看一下AtomicInteger是如何利用CAS實現原子性操作的。爲了形成更新鮮明的對比,先來看一下如果不使用CAS機制,想實現線程安全我們通常如何處理。

在沒有使用CAS機制時,爲了保證線程安全,基於synchronized的實現如下:

public class ThreadSafeTest {

	public static volatile int i = 0;

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

至於上面的實例具體實現,這裏不再展開,很多相關的文章專門進行講解,我們只需要知道爲了保證i++的原子操作,在increase方法上使用了重量級的鎖synchronized,這會導致該方法的性能低下,所有調用該方法的操作都需要同步等待處理。

那麼,如果採用基於CAS實現的AtomicInteger類,上述方法的實現便變得簡單且輕量級了:

public class ThreadSafeTest {

	private final AtomicInteger counter = new AtomicInteger(0);

	public int increase(){
		return counter.addAndGet(1);
	}

}

之所以可以如此安全、便捷的來實現安全操作,便是由於AtomicInteger類採用了CAS機制。下面,我們就來了解一下AtomicInteger的功能及源碼實現。

CAS的AtomicInteger類

AtomicInteger是java.util.concurrent.atomic 包下的一個原子類,該包下還有AtomicBoolean, AtomicLong,AtomicLongArray, AtomicReference等原子類,主要用於在高併發環境下,保證線程安全。

AtomicInteger常用API

AtomicInteger類提供瞭如下常見的API功能:

public final int get():獲取當前的值
public final int getAndSet(int newValue):獲取當前的值,並設置新的值
public final int getAndIncrement():獲取當前的值,並自增
public final int getAndDecrement():獲取當前的值,並自減
public final int getAndAdd(int delta):獲取當前的值,並加上預期的值
void lazySet(int newValue): 最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。

上述方法中,getAndXXX格式的方法都實現了原子操作。具體的使用方法參考上面的addAndGet案例即可。

AtomicInteger核心源碼

下面看一下AtomicInteger代碼中的核心實現代碼:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            // 用於獲取value字段相對當前對象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    //返回當前值
    public final int get() {
        return value;
    }

    //遞增加detla
    public final int getAndAdd(int delta) {
        // 1、this:當前的實例 
        // 2、valueOffset:value實例變量的偏移量 
        // 3、delta:當前value要加上的數(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    //遞增加1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

上述代碼以AtomicInteger#incrementAndGet方法爲例展示了AtomicInteger的基本實現。其中,在static靜態代碼塊中,基於Unsafe類獲取value字段相對當前對象的“起始地址”的偏移量,用於後續Unsafe類的處理。

在處理自增的原子操作時,使用的是Unsafe類中的getAndAddInt方法,CAS的實現便是由Unsafe類的該方法提供,從而保證自增操作的原子性。

同時,在AtomicInteger類中,可以看到value值通過volatile進行修飾,保證了該屬性值的線程可見性。在多併發的情況下,一個線程的修改,可以保證到其他線程立馬看到修改後的值。

通過源碼可以看出, AtomicInteger 底層是通過volatile變量和CAS兩者相結合來保證更新數據的原子性。其中關於Unsafe類對CAS的實現,我們下面詳細介紹。

CAS的工作原理

CAS的實現原理簡單來說就是由Unsafe類和其中的自旋鎖來完成的,下面針對源代碼來看一下這兩塊的內容。

UnSafe類

在AtomicInteger核心源碼中,已經看到CAS的實現是通過Unsafe類來完成的,先來了解一下Unsafe類的作用。關於Unsafe類在之前的文章《各大框架都在使用的Unsafe類,到底有多神奇?》也有詳細的介紹,大家可以參考,這裏我們再簡單概述一下。

sun.misc.Unsafe是JDK內部用的工具類。它通過暴露一些Java意義上說“不安全”的功能給Java層代碼,來讓JDK能夠更多的使用Java代碼來實現一些原本是平臺相關的、需要使用native語言(例如C或C++)纔可以實現的功能。該類不應該在JDK核心類庫之外使用,這也是命名爲Unsafe(不安全)的原因。

JVM的實現可以自由選擇如何實現Java對象的“佈局”,也就是在內存裏Java對象的各個部分放在哪裏,包括對象的實例字段和一些元數據之類。

Unsafe裏關於對象字段訪問的方法把對象佈局抽象出來,它提供了objectFieldOffset()方法用於獲取某個字段相對Java對象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之類的方法可以使用前面獲取的偏移量來訪問某個Java對象的某個字段。在AtomicInteger的static代碼塊中便使用了objectFieldOffset()方法。

Unsafe類的功能主要分爲內存操作、CAS、Class相關、對象操作、數組相關、內存屏障、系統相關、線程調度等功能。這裏我們只需要知道其功能即可,方便理解CAS的實現,注意不建議在日常開發中使用。

Unsafe與CAS

AtomicInteger調用了Unsafe#getAndAddInt方法:

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

上述代碼等於是AtomicInteger調用UnSafe類的CAS方法,JVM幫我們實現出彙編指令,從而實現原子操作。

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

getAndAddInt方法有三個參數:

  • 第一個參數表示當前對象,也就是new的那個AtomicInteger對象;
  • 第二個表示內存地址;
  • 第三個表示自增步伐,在AtomicInteger#incrementAndGet中默認的自增步伐是1。

getAndAddInt方法中,首先把當前對象主內存中的值賦給val5,然後進入while循環。判斷當前對象此刻主內存中的值是否等於val5,如果是,就自增(交換值),否則繼續循環,重新獲取val5的值。

在上述邏輯中核心方法是compareAndSwapInt方法,它是一個native方法,這個方法彙編之後是CPU原語指令,原語指令是連續執行不會被打斷的,所以可以保證原子性。

在getAndAddInt方法中還涉及到一個實現自旋鎖。所謂的自旋,其實就是上面getAndAddInt方法中的do while循環操作。當預期值和主內存中的值不等時,就重新獲取主內存中的值,這就是自旋。

這裏我們可以看到CAS實現的一個缺點:內部使用自旋的方式進行CAS更新(while循環進行CAS更新,如果更新失敗,則循環再次重試)。如果長時間都不成功的話,就會造成CPU極大的開銷。

另外,Unsafe類還支持了其他的CAS方法,比如compareAndSwapObject compareAndSwapIntcompareAndSwapLong。更多關於Unsafe類的功能就不再展開,大家可以去看《各大框架都在使用的Unsafe類,到底有多神奇?》這篇文章。

CAS的缺點

CAS高效的實現了原子性操作,但在以下三方面還存在着一些缺點:

  • 循環時間長,開銷大;
  • 只能保證一個共享變量的原子操作;
  • ABA問題;

下面就這個三個問題詳細討論一下。

循環時間長開銷大

在分析Unsafe源代碼的時候我們已經提到,在Unsafe的實現中使用了自旋鎖的機制。在該環節如果CAS操作失敗,就需要循環進行CAS操作(do while循環同時將期望值更新爲最新的),如果長時間都不成功的話,那麼會造成CPU極大的開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升。

只能保證一個共享變量的原子操作

在最初的實例中,可以看出是針對一個共享變量使用了CAS機制,可以保證原子性操作。但如果存在多個共享變量,或一整個代碼塊的邏輯需要保證線程安全,CAS就無法保證原子性操作了,此時就需要考慮採用加鎖方式(悲觀鎖)保證原子性,或者有一個取巧的辦法,把多個共享變量合併成一個共享變量進行CAS操作。

ABA問題

雖然使用CAS可以實現非阻塞式的原子性操作,但是會產生ABA問題,ABA問題出現的基本流程:

  • 進程P1在共享變量中讀到值爲A;
  • P1被搶佔了,進程P2執行;
  • P2把共享變量裏的值從A改成了B,再改回到A,此時被P1搶佔;
  • P1回來看到共享變量裏的值沒有被改變,於是繼續執行;

雖然P1以爲變量值沒有改變,繼續執行了,但是這個會引發一些潛在的問題。ABA問題最容易發生在lock free的算法中的,CAS首當其衝,因爲CAS判斷的是指針的地址。如果這個地址被重用了呢,問題就很大了(地址被重用是很經常發生的,一個內存分配後釋放了,再分配,很有可能還是原來的地址)。

維基百科上給了一個形象的例子:你拿着一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女,然後她很暖昧地挑逗着你,並趁你不注意,把用一個一模一樣的手提箱和你那裝滿錢的箱子調了個包,然後就離開了,你看到你的手提箱還在那,於是就提着手提箱去趕飛機去了。

ABA問題的解決思路就是使用版本號:在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A->B->A就會變成1A->2B->3A。

另外,從Java 1.5開始,JDK的Atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

小結

本文從CAS的基本使用場景、基本流程、實現類AtomicInteger源碼解析、CAS的Unsafe實現解析、CAS的缺點及解決方案等方面來全面瞭解了CAS。通過這篇文章的學習想必你已經更加深刻的理解CAS機制了,如果對你有所幫助,記得關注一下,持續輸出乾貨內容。

博主簡介:《SpringBoot技術內幕》技術圖書作者,酷愛鑽研技術,寫技術乾貨文章。

公衆號:「程序新視界」,博主的公衆號,歡迎關注~

技術交流:請聯繫博主微信號:zhuan2quan


微信公衆號:程序新視界

程序新視界”,一個100%技術乾貨的公衆號

本文同步分享在 博客“程序新視界”(CSDN)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章