和前女友複合的那個晚上,我徹底想通了CAS

程序員讀書俱樂部 發佈 2021-09-27T18:26:47+00:00

這是一個傷感的微故事當年忽然消失,拿走我2w元的前女友又忽然回來找我。我心情是複雜的,本以為她要還錢,沒想到是複合。我們長談到深夜,主要是她在聊自己的經歷,我聽完, 好言相勸她還錢,她甩給我2w,我假裝生氣,奪門而出。現已物是人非,2w塊錢還是很香,像撿來的一樣。

這是一個傷感的微故事

當年忽然消失,拿走我2w元的前女友又忽然回來找我。我心情是複雜的,本以為她要還錢,沒想到是複合。我們長談到深夜,主要是她在聊自己的經歷,我聽完, 好言相勸她還錢,她甩給我2w,我假裝生氣,奪門而出。現已物是人非,2w塊錢還是很香,像撿來的一樣。

出來抽菸的我似乎想通了CAS的ABA問題。

什麼是CAS

CAS(Compare And Swap),比較替換。它有3個核心參數:

  1. V:要修改的變量
  2. A :V的原有值,即線程讀取V時的值
  3. B:要修改成的值

在對V做修改之前,先檢查V的值是否與預期值A相等,如果相等就把V值更新為B,否則不更新,CAS失敗。

下面是一段偽代碼幫助大家理解

//把a的值從3改成4,這裡V就是a,A是3,B是4
int a = 3;
cas(a,3,4);

先用3與a此刻的值(a所指向的內存值)比較,如果相等,則a會被修改成4。否則修改失敗。失敗後如何處理?

它常與自旋(通常用死循環實現)配合使用,修改失敗則不停重試CAS,直到成功。整個過程無需鎖定a,是一種樂觀鎖的實現。檢測

衝突和數據更新本身也是樂觀鎖的一貫作風。

解決線程同步

解決線程同步的方式一般有2種

  1. 悲觀鎖
  2. 樂觀鎖

悲觀鎖的缺陷很明顯,比如死鎖、加鎖釋放鎖帶來的性能消耗。

用樂觀鎖在大部分情況下可以提高性能,上面說的CAS就是一種樂觀鎖的實現。但CAS只能解決多線程的原子性,並不能解決另外2個問

題,順序性和一致性。而volatile正好能解決這2個問題,你說巧不巧。所以CAS和volatile必定能走到一起。

當然java大師們早已發現了其中的奧秘。用CAS+volatile作為基石開發出了大名鼎鼎的J.U.C包。

其中最簡單直觀的要數atomic包。

多線程場景下,給某個數做自增的時候通常是加synchronized關鍵字

int count = 0;
synchronized void c() {
  for (int i = 0; i < 10000; i++)
   count++;
}

JUC包中的AtomicInteger提供的incrementAndGet 完全可以代替synchronized和count++的功能。

AtomicInteger count = new AtomicInteger(0);
void c() {
 for (int i = 0; i < 10000; i++)
   count.incrementAndGet();
}

AtomicInteger:

//value被volatile修飾
private volatile int value;
public AtomicInteger(int initialValue) {
        value = initialValue;
}


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

Unsafe:

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

count.incrementAndGet最終調用的方法是Unsafe類的compareAndSwapInt方法,這個方法被native修飾。也就意味著它已經調用了

虛擬機C/C++實現的代碼。筆者試著跟了一下Hotspot的代碼,具體過程就不演示了。最後代碼如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint*dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

關鍵看LOCK_IF_MP:如果是多處理器要加lock,保證CAS的原子性,最終執行的是彙編指令:cmpxchg(compare and exchange)。

lock cmpxchg

彙編是可以直接操作CPU的,cmpxchg指令也是cpu支持的原語。但cmpxchg並不是原子的,所以在多處理器下還需要加lock來保證CAS的原子性。

有同學說了,這還不是加鎖了。是的,只是在系統級別加的鎖,要比在jvm上加鎖效率高的多。

我們說的CAS無鎖並不是絕對的無鎖,而是在java層面表現為無鎖。

看不懂c++、彙編指令不要緊,但是lock cmpxchg 一定要記住,因為面試要問。

CAS的ABA問題

ABA :數據從A變成B,又變回A,假裝什麼都沒發生。

上面的例子中,如果a從3變為5,又變回3,那麼CAS在對比時a的值依然是3,替換成功。

這好像也沒什麼問題,反正目的就是把a的值改成4。就像前女友還我的2萬塊錢,雖然RMB已經改版,新版2w依然可以接受。

但不是所有人都能接受失而復得。

一般基礎類型的ABA問題大家都能接受。但是複雜類型就不一定了,比如下面的Zoo類

public class Zoo {
    private Tiger tiger = new Tiger("母老虎");
    public static void main(String[] args) {
        Zoo zoo = new Zoo();
        zoo.setTiger(new Tiger("公老虎"));
    }
}

Zoo裡面有成員變量tiger。當new一個Zoo實例zoo的時候,zoo的內存值已經確定了,不會因為其成員變量的改變而改變。

這樣的ABA問題就比較嚴重。就像我的前任一樣。

解決ABA問題通用的辦法就是加版本號,A->B->A改成A1-B2->A3。JUC用帶版本號的 AtomicStampedReference 來解決ABA問題。

//initialRef:要修改的引用V,initialStamp:版本號,可以是時間戳或任何整數
public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

本文著重是理論分析,而且AtomicStampedReference比較簡單,就不做展開討論。

ABA終於想通了 ,多麼痛的領悟,你的前任回來找你了嗎。記得注意是否存在ABA問題哦。

(本故事純屬虛構,如有雷同,請不要對號入座)。

關鍵字: