CAS原理分析及ABA問題詳解

image

什麼是CAS

CASCompare And Swap的縮寫,翻譯成中文就是比較並交換,其做用是讓CPU比較內存中某個值是否和預期的值相同,若是相同則將這個值更新爲新值,不相同則不作更新,也就是CAS是原子性的操做(讀和寫二者同時具備原子性),其實現方式是經過藉助C/C++調用CPU指令完成的,因此效率很高。
CAS的原理很簡單,這裏使用一段Java代碼來描述java

public boolean compareAndSwap(int value, int expect, int update) {
//        若是內存中的值value和指望值expect同樣 則將值更新爲新值update
    if (value == expect) {
        value = update;
        return true;
    } else {
        return false;
    }
}
複製代碼

大體過程是將內存中的值、咱們的指望值、新值交給CPU進行運算,若是內存中的值和咱們的指望值相同則將值更新爲新值,不然不作任何操做。這個過程是在CPU中完成的,這裏很差描述CPU的工做過程,就拿Java代碼來描述了。segmentfault

Unsafe源碼分析

Java是在Unsafe(sun.misc.Unsafe)類實現CAS的操做,而咱們知道Java是沒法直接訪問操做系統底層的API的(緣由是Java的跨平臺性限制了Java不能和操做系統耦合),因此Java並無在Unsafe類直接實現CAS的操做,而是經過**JDI(Java Native Interface)**本地調用C/C++語言來實現CAS操做的。
Unsafe有不少個CAS操做的相關方法,這裏舉例幾個安全

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
複製代碼

咱們拿public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);進行分析,這個方法是比較內存中的一個值(整型)和咱們的指望值(var4)是否同樣,若是同樣則將內存中的這個值更新爲var5,參數中的var1是值所在的對象,var2是值在對象(var1)中的內存偏移量,參數var1和參數var2是爲了定位出值所在內存的地址
bash

Unsafe.java在這裏發揮的做用有:

  1. 將對象引用、值在對象中的偏移量、指望的值和欲更新的新值傳遞給Unsafe.cpp
  2. 若是值更新成功則返回true給開發者,沒有更新則返回false

Unsafe.cpp在這裏發揮的做用有:多線程

  1. 接受從Unsafe傳遞過來的對象引用、偏移量、指望的值和欲更新的新值,根據對象引用和偏移量計算出值的地址,而後將值的地址、指望的值、欲更新的新值傳遞給CPU
  2. 若是值更新成功則返回trueUnsafe.java,沒有更新則返回false

CPU在這裏發揮的做用:源碼分析

  1. 接受從Unsafe.cpp傳遞過來的地址、指望的值和欲更新的新值,執行指令cmpxchg,比較地址中的值是否和指望的值同樣,同樣則將值更新爲新的值,不同則不作任何操做
  2. 將操做結果返回給Unsafe.cpp

CAS的缺點

CAS雖然高效的實現了原子性操做,可是也存在一些缺點,主要表如今如下三個方面。ui

ABA問題

在多線程場景下CAS會出現ABA問題,關於ABA問題這裏簡單科普下,例若有2個線程同時對同一個值(初始值爲A)進行CAS操做,這三個線程以下spa

  1. 線程1,指望值爲A,欲更新的值爲B
  2. 線程2,指望值爲A,欲更新的值爲B

線程1搶先得到CPU時間片,而線程2由於其餘緣由阻塞了,線程1取值與指望的A值比較,發現相等而後將值更新爲B,而後這個時候出現了線程3,指望值爲B,欲更新的值爲A,線程3取值與指望的值B比較,發現相等則將值更新爲A,此時線程2從阻塞中恢復,而且得到了CPU時間片,這時候線程2取值與指望的值A比較,發現相等則將值更新爲B,雖然線程2也完成了操做,可是線程2並不知道值已經通過了A->B->A的變化過程。操作系統

ABA問題帶來的危害
小明在提款機,提取了50元,由於提款機問題,有兩個線程,同時把餘額從100變爲50
線程1(提款機):獲取當前值100,指望更新爲50,
線程2(提款機):獲取當前值100,指望更新爲50,
線程1成功執行,線程2某種緣由block了,這時,某人給小明匯款50
線程3(默認):獲取當前值50,指望更新爲100,
這時候線程3成功執行,餘額變爲100,
線程2從Block中恢復,獲取到的也是100,compare以後,繼續更新餘額爲50!!!
此時能夠看到,實際餘額應該爲100(100-50+50),可是實際上變爲了50(100-50+50-50)這就是ABA問題帶來的成功提交。線程

解決方法: 在變量前面加上版本號,每次變量更新的時候變量的版本號都+1,即A->B->A就變成了1A->2B->3A

循環時間長開銷大

若是CAS操做失敗,就須要循環進行CAS操做(循環同時將指望值更新爲最新的),若是長時間都不成功的話,那麼會形成CPU極大的開銷。

這種循環也稱爲自旋

解決方法: 限制自旋次數,防止進入死循環。

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

CAS的原子操做只能針對一個共享變量。

解決方法: 若是須要對多個共享變量進行操做,可使用加鎖方式(悲觀鎖)保證原子性,或者能夠把多個共享變量合併成一個共享變量進行CAS操做。

CAS的應用

咱們知道CAS操做並不會鎖住共享變量,也就是一種非阻塞的同步機制,CAS就是樂觀鎖的實現。

  1. 樂觀鎖 樂觀鎖老是假設最好的狀況,每次去操做數據都認爲不會被別的線程修改數據,因此在每次操做數據的時候都不會給數據加鎖,即在線程對數據進行操做的時候,別的線程不會阻塞仍然能夠對數據進行操做,只有在須要更新數據的時候纔會去判斷數據是否被別的線程修改過,若是數據被修改過則會拒絕操做而且返回錯誤信息給用戶。
  2. 悲觀鎖 悲觀鎖老是假設最壞的狀況,每次去操做數據時候都認爲會被的線程修改數據,因此在每次操做數據的時候都會給數據加鎖,讓別的線程沒法操做這個數據,別的線程會一直阻塞直到獲取到這個數據的鎖。這樣的話就會影響效率,好比當有個線程發生一個很耗時的操做的時候,別的線程只是想獲取這個數據的值而已都要等待好久。

Java利用CAS的樂觀鎖、原子性的特性高效解決了多線程的安全性問題,例如JDK1.8中的集合類ConcurrentHashMap、關鍵字volatileReentrantLock等。

參考

JAVA CAS原理深度分析
Java CAS 原理分析
什麼是ABA問題?

原文地址:ddnd.cn/2019/03/13/…

相關文章
相關標籤/搜索