【Java多線程】 CAS —— 一文了解CAS到底是什麼

學過多線程就會接觸到併發,併發再多線程中的重要性不言而喻,在Java中還有併發包,裏面實現了各種各樣的方法來幫助我們解決多線程帶來的各種問題。而要想讀懂這些底層問題,CAS是繞不過的知識,大多底層都是以CAS來實現的。今天就帶大家來學習CAS相關的知識。

一、什麼是CAS?

CAS: 全稱Compare and swap,字面意思:" 比較並交換 "
一個CAS涉及到的操作數有:

  • 內存值V
  • 舊的預期值A
  • 要修改的新值B

有了這三個操作數,在看看它的操作:

  1. 首先輸入舊的預期值A 和修改後的新值 B
  2. 對比變量是舊值和內存值是否相同
  3. 如果相同,將舊值改爲新值

這也就側面的表現出了對於多個線程都對某個值進行修改時,保證了修改前拿到的值是期望值纔會操作。

二、爲什麼要有CAS

還記得之前多線程的經典例子嘛,就是多個線程同時對一個共享變量進行修改值,最終修改後的值大概率不是正確的結果。也就是對於i ++ 操作,在多線程中是保證不了它的正確性。
原因呢,就是i ++ 本身並不是一個原子性的操作,它可以分成三步:

  1. 從主內存中讀取到 i 的值
  2. 對 i 進行+1 操作
  3. 寫回到主內存

這就導致多線程在執行該操作時,線程A、B可能同時從主內存中獲得一個值後分別 +1 後寫回到主內存。導致結果的錯誤,這時候你就會想到 那用 唄! 所以解決的方式就是使用synchronized關鍵字來進行加鎖。
的確,這種多線程導致的原子性問題可以加鎖,使得衆多線程競爭鎖,拿到鎖的線程纔可以進行下一步的操作,其它線程則都開始阻塞,直到這個線程釋放了鎖後,喚醒其它線程,再次開始競爭鎖。
對於線程的阻塞和喚醒都是非常消耗時間的! 如果像i ++ 的操作,僅僅只是每次進行加一操作就要經歷線程喚醒和重新競爭鎖,未免有些大材小用。

而這時候,就可以使用CAS機制中的compareAndSet方法,也就是比較並設值。
當多個線程同時對某個資源進行CAS操作,只能有一個線程操作成功,但是並不會阻塞其他線程,其他線程只會收到操作失敗的信號。可見 CAS 其實是一個樂觀鎖

三、CAS是怎麼實現的?

看了上面的解釋,你可能還會有些疑惑,CAS怎麼保證的在設置值的時候的原子性呢?看起來也是進行了比較值和設置值的操作呀?
其實,這都是操作系統的功勞!在操作系統中這麼多操作實際上就是一條指令操作。

針對不同的操作系統,JVM 用到了不同的 CAS 實現原理:

  • java 的 CAS 利用的的是 unsafe 這個類提供的 CAS 操作;
  • unsafe 的 CAS 依賴了的是 jvm 針對不同的操作系統實現的 Atomic::cmpxchg;
  • Atomic::cmpxchg 的實現使用了彙編的 CAS 操作,並使用 cpu 硬件提供的 lock 機制保證其原子性。

看起來很複雜,其實只要知道:是因爲硬件予以了支持,軟件層面才能做到。

四、CAS有哪些應用?

實現原子類
Java中的atomic包下的原子類,都是通過CAS實現的。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    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;
    }

可以參考另一篇文章——> Java中atomic包下的原子操作類

五、CAS存在的問題

  1. 典型的就是ABA的問題,也就是先將值改變,再改回去,看起來好像就是沒有改變一樣,CAS也發現不了這個過程。 對於基本類型的值來說,這種把數字改變了在改回原來的值是沒有太大影響的,但如果是對於引用類型的話,就會產生很大的影響了
    📌解決這個問題就是加入版本信息,也就是每一個線程修改值後,版本信息都會改變,這樣即使兩個線程是持有相同引用,但版本信息卻不一致,我們也認爲是不一樣的。這樣也可以防止ABA問題。在Java中的 AtomicStampedReference 這個類是可以提供版本控制的。
  2. 對於線程過多的問題,當太多線程進行CAS,每次判斷都會發現值已經不再是期望的原始值,這就會導致很多線程是在白白的空轉,效率降低。
    📌爲了解決這個問題Java8 引入了一個 cell[] 數組,線程少時,就使用CAS機制。當過多的線程請求時,就會將多個線程分組,並將cell中的元素分配給某一組線程,而這組線程對數的操作就會在cell中進行,到最後,將cell中的元素在進行合併。

由以上着問題我們也可以看出來,CAS由於其不會阻塞線程的特點,而是一直在循環,這就使得它的適用場景就是代碼能很快的執行,如果代碼執行時間過長,就會導致其它線程調用方法長時間處於失敗狀態。

好啦,這就是CAS的一些基本的知識了,自己總結的也還有些不到位,如果大家感興趣,建議研究源碼,可以學到很多。如果文章有什麼問題,歡迎留言指正。也歡迎點贊關注一起進步😀

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