1. 程式人生 > >Java併發/多執行緒-CAS原理分析

Java併發/多執行緒-CAS原理分析

[toc] # 什麼是CAS CAS 即 compare and swap,比較並交換。 CAS是一種原子操作,同時 CAS 使用樂觀鎖機制。 J.U.C中的很多功能都是建立在 CAS 之上,各種原子類,其底層都用 CAS來實現原子操作。用來解決併發時的安全問題。 # 併發安全問題 ## 舉一個典型的例子`i++` ```java public class AddTest { public volatile int i; public void add() { i++; } } ``` 通過`javap -c AddTest`可以看到add 方法的位元組碼指令: ```java public void add(); Code: 0: aload_0 1: dup 2: getfield #2 // Field i:I 5: iconst_1 6: iadd 7: putfield #2 // Field i:I 10: return ``` `i++`被拆分成了多個指令: 1. 執行`getfield`拿到原始記憶體值; 2. 執行`iadd`進行加 1 操作; 3. 執行`putfield`寫把累加後的值寫回記憶體。 **假設一種情況:** - 當`執行緒 1` 執行到`iadd`時,由於還沒有執行`putfield`,這時候並不會重新整理主記憶體區中的值。 - 此時`執行緒 2` 進入開始執行,剛剛將主記憶體區的值拷貝到私有記憶體區。 - `執行緒 1`正好執行`putfield`,更新主記憶體區的值,那麼此時`執行緒 2` 的副本就是舊的了。錯誤就出現了。 ## 如何解決? 最簡單的,在 add 方法加上 synchronized 。 ```java public class AddTest { public volatile int i; public synchronized void add() { i++; } } ``` 雖然簡單,並且解決了問題,但是效能表現並不好。 最優的解法應該是使用JDK自帶的**CAS**方案,如上例子,使用`AtomicInteger`類 ```java public class AddIntTest { public AtomicInteger i; public void add() { i.getAndIncrement(); } } ``` ## 底層原理 **CAS 的原理並不複雜:** - **三個引數,一個當前記憶體值 V、預期值 A、更新值 B** - **當且僅當預期值 A 和記憶體值 V 相同時,將記憶體值修改為 B 並返回 true** - **否則什麼都不做,並返回 false** 拿 `AtomicInteger` 類分析,先來看看原始碼: > 我這裡的環境是Java11,如果是Java8這裡一些內部的一些命名有些許不同。 ```java public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; /* * This class intended to be implemented using VarHandles, but there * are unresolved cyclic startup dependencies. */ private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe(); private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); private volatile int value; //... } ``` `Unsafe` 類,該類對一般開發而言,少有用到。 `Unsafe` 類底層是用 C/C++ 實現的,所以它的方式都是被 native 關鍵字修飾過的。 它可以提供硬體級別的原子操作,如獲取某個屬性在記憶體中的位置、修改物件的欄位值。 **關鍵點:** - `AtomicInteger` 類儲存的值在 `value` 欄位中,而`value`欄位被`volatile` - 在靜態程式碼塊中,並且獲取了 `Unsafe` 例項,獲取了 `value` 欄位在記憶體中的偏移量 `VALUE` 接下回到剛剛的例子: 如上,`getAndIncrement()` 方法底層利用 CAS 技術保證了併發安全。 ```java public final int getAndIncrement() { return U.getAndAddInt(this, VALUE, 1); } ``` `getAndAddInt()` 方法: ```java public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!weakCompareAndSetInt(o, offset, v, v + delta)); return v; } ``` `v` 通過 `getIntVolatile(o, offset)`方法獲取,其目的是獲取 `o` 在 `offset` 偏移量的值,其中 `o` 就是 `AtomicInteger` 類儲存的值,即`value`, `offset` 記憶體偏移量的值,即 `VALUE`。 **重點**,`weakCompareAndSetInt` 就是實現 **CAS 的核心方法** - 如果 `o` 和 `v`相等,就證明沒有其他執行緒改變過這個變數,那麼就把 `v` 值更新為 `v + delta`,其中 `delta` 是更新的增量值。 - 反之 CAS 就一直採用自旋的方式繼續進行操作,這一步也是一個原子操作。 **分析:** - 設定 `AtomicInteger` 的原始值為 A,`執行緒 1` 和`執行緒 2` 各自持有一份副本,值都是 A。 1. `執行緒 1` 通過`getIntVolatile(o, offset)`拿到 value 值 A,這時`執行緒 1` 被掛起。 2. `執行緒 2` 也通過`getIntVolatile(o, offset)`方法獲取到 value 值 A,並執行`weakCompareAndSetInt`方法比較記憶體值也為 A,成功修改記憶體值為 B。 3. 這時`執行緒 1` 恢復執行`weakCompareAndSetInt`方法比較,發現自己手裡的值 A 和記憶體的值 B 不一致,說明該值已經被其它執行緒提前修改過了。 4. `執行緒 1` 重新執行`getIntVolatile(o, offset)`再次獲取 value 值,因為變數 value 被 volatile 修飾,具有可見性,執行緒A繼續執行`weakCompareAndSetInt`進行比較替換,直到成功 # CAS需要注意的問題 ## 使用限制 CAS是由CPU支援的原子操作,其原子性是在硬體層面進行保證的,在Java中普通使用者無法直接使用,只能藉助`atomic`包下的原子類使用,靈活性受限。 但是CAS只能保證單個變數操作的原子性,當涉及到多個變數時,CAS無能為力。 原子性也不一定能保證執行緒安全,如在Java中需要與`volatile`配合來保證執行緒安全。 ## ABA 問題 ### 概念 CAS 有一個問題,舉例子如下: - `執行緒 1` 從記憶體位置 V 取出 A - 這時候`執行緒 2` 也從記憶體位置 V 取出 A - 此時`執行緒 1` 處於掛起狀態,`執行緒 2` 將位置 V 的值改成 B,最後再改成 A - 這時候`執行緒 1` 再執行,發現位置 V 的值沒有變化,符合期望繼續執行。 此時雖然`執行緒 1`還是成功了,但是這並不符合我們真實的期望,等於`執行緒 2`**狸貓換太子**把`執行緒 1`耍了。 **這就是所謂的ABA問題** #### 解決方案 > 引入原子引用,帶版本號的原子操作。 把我們的每一次操作都帶上一個版本號,這樣就可以避免ABA問題的發生。既樂觀鎖的思想。 - 記憶體中的值每發生一次變化,版本號都更新。 - 在進行CAS操作時,比較記憶體中的值的同時,也會比較版本號,只有當二者都沒有變化時,才能執行成功。 - Java中的`AtomicStampedReference`類便是使用版本號來解決ABA問題的。 ## 高競爭下的開銷問題 - 在併發衝突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。 - 針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值後失敗退出。 - 更重要的是避免在高競爭環境下使用樂觀鎖