CAS原理分析及ABA問題詳解

薛8發表於2019-03-13
image

什麼是CAS

CASCompare And Swap的縮寫,翻譯成中文就是比較並交換,其作用是讓CPU比較記憶體中某個值是否和預期的值相同,如果相同則將這個值更新為新值,不相同則不做更新,也就是CAS是原子性的操作(讀和寫兩者同時具有原子性),其實現方式是通過藉助C/C++呼叫CPU指令完成的,所以效率很高。
CAS的原理很簡單,這裡使用一段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程式碼來描述了。

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是為了定位出值所在記憶體的地址

CAS原理分析及ABA問題詳解
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雖然高效的實現了原子性操作,但是也存在一些缺點,主要表現在以下三個方面。

ABA問題

在多執行緒場景下CAS會出現ABA問題,關於ABA問題這裡簡單科普下,例如有2個執行緒同時對同一個值(初始值為A)進行CAS操作,這三個執行緒如下

  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/…

相關文章