聊聊併發(六)——CAS演算法

L發表於2021-12-02

一、原子類

1、CAS演算法

  強烈建議讀者看這篇之前,先看這篇 初識JUC 的前兩節,對原子性,原子變數,記憶體可見性有一個初步認識。

  CAS(Compare and Swap)是一種硬體對併發的支援,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享資料的併發訪問,是硬體對於併發操作共享資料的支援。它是一個原子性的操作,對應到CPU指令為cmpxchg。它是一條CPU併發原語。
  CAS包含了3個運算元:記憶體值V,比較值A,更新值B。當且僅當V == A時,V = B,否則不執行任何操作。
  CAS演算法:當多個執行緒併發的對主存中的資料進行修改的時候。有且只有一個執行緒會成功,其他的都會失敗(同時操作,只是會失敗而已,並不會被鎖之類的)。
  CAS是一種無鎖的非阻塞演算法,是樂觀鎖的一種實現。不存在上下文切換的問題。
  CAS比普通同步鎖效率高,原因:CAS演算法當這一次不成功的時候,它下一次不會阻塞,也就是它不會放棄CPU的執行權,它可以立即再次嘗試,再去更新。
  通俗的說:我要將變數 i 由 2 修改為 3。當記憶體中 i == 2,且修改成功,才為成功。若記憶體中 i 由於其他執行緒的操作已經不是 2 了,那此次我的修改視為失敗。

2、簡單使用

  JDK 1.5 以後java.util.concurrent.atomic包下提供了常用的原子變數。它支援單個變數上的無鎖執行緒安全程式設計。這些原子變數具備以下特點:volatile的記憶體可見性;CAS演算法保證資料的原子性。

  atomic包描述:圖片來源API文件

  程式碼示例:原子變數使用

 1 public class Main {
 2     public static void main(String[] args) {
 3         AtomicInteger integer = new AtomicInteger(2);
 4 
 5         boolean b = integer.compareAndSet(3, 5);
 6         System.out.println(b);
 7         System.out.println(integer.get());
 8 
 9         b = integer.compareAndSet(2, 10);
10         System.out.println(b);
11         System.out.println(integer.get());
12 
13         // 等價於 i++
14         integer.getAndIncrement();
15 
16         // 等價於 ++i
17         integer.incrementAndGet();
18     }
19 }
20 
21 // 結果
22 false
23 2
24 true
25 10

  分析:很簡單,設定初始值為 2。
  ①由 3 修改成5,而設定初始值記憶體值為2,所以修改失敗,返回false。
  ②由 2 修改成10,初始值記憶體值為2,所以修改成功,返回true。

3、原始碼分析

  這些原子變數底層就是通過CAS演算法來保證資料的原子性。
  原始碼示例:AtomicInteger 類

 1 public class AtomicInteger extends Number implements java.io.Serializable {
 2     private static final long serialVersionUID = 6214790243416807050L;
 3 
 4     // setup to use Unsafe.compareAndSwapInt for updates
 5     private static final Unsafe unsafe = Unsafe.getUnsafe();
 6     private static final long valueOffset;
 7 
 8     // 獲取value在記憶體的地址偏移量
 9     static {
10         try {
11             valueOffset = unsafe.objectFieldOffset
12                 (AtomicInteger.class.getDeclaredField("value"));
13         } catch (Exception ex) { throw new Error(ex); }
14     }
15 
16     private volatile int value;
17 
18     public AtomicInteger(int initialValue) {
19         value = initialValue;
20     }
21 
22     public AtomicInteger() {
23     }
24 
25     public final int get() {
26         return value;
27     }
28 
29     public final void set(int newValue) {
30         value = newValue;
31     }
32 
33     public final boolean compareAndSet(int expect, int update) {
34         return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
35     }
36 
37     public final int getAndIncrement() {
38         return unsafe.getAndAddInt(this, valueOffset, 1);
39     }
40 
41     public final int incrementAndGet() {
42         return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
43     }
44 
45 }

  說明:public final boolean compareAndSet(int expect, int update)
  變數valueOffset:通過靜態程式碼塊獲取變數value在記憶體中的偏移地址。
  變數value:用volatile修飾,這裡體現了"多執行緒之間的記憶體可見性"。
  this:即 AtomicInteger 物件本身。
  很容易理解:就是將當前物件 this 的變數value,由期望值 expect 修改為 update。

  原始碼示例:Unsafe 類

 1 public final class Unsafe {
 2 
 3     public native void throwException(Throwable var1);
 4 
 5     public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
 6 
 7     public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 8 
 9     public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
10 
11     public native int getIntVolatile(Object var1, long var2);
12 
13 
14     public final int getAndAddInt(Object var1, long var2, int var4) {
15         int var5;
16         do {
17             // 獲取物件var1的變數var2的記憶體值
18             var5 = this.getIntVolatile(var1, var2);
19         } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
20 
21         return var5;
22     }
23 
24 }

  Unsafe是CAS的核心類,其所有方法都是native修飾的。也就是說Unsafe類中的方法都直接呼叫作業系統底層資源執行相應任務,是由C/C++編寫的本地方法。CAS演算法的實現,也是由Unsafe類通過呼叫本地方法直接操作特定記憶體資料來實現的。
  getAndIncrement()方法能夠在多執行緒環境保證變數的原子性自增。但原始碼中,並沒有加synchronized或者lock鎖,那麼,它是如何保證的呢?其實很簡單:

  先獲取一次變數的記憶體值,然後通過CAS演算法進行比較更新。失敗了就一直不停的重試,是一個迴圈的過程,這個過程也稱作自旋。
  這就是為什麼 AtomicInteger 的自增操作具備原子性。

1 private AtomicInteger i = new AtomicInteger();
2 public int getI() {
3     return i.getAndIncrement();
4 }

4、CAS的缺點

  (1)ABA問題。
  (2)迴圈時間變長:高併發情況下,使用CAS可能會存在一些執行緒一直迴圈修改不成功,導致迴圈時間變長,這會給CPU帶來很大的執行開銷。由於AtomicInteger中的變數是volatile的,為了保證記憶體可見性,需要保證快取一致性,通過匯流排傳輸資料,當有大量的CAS迴圈時,會產生匯流排風暴。
  (3)只能保證一個變數的原子操作:如果需要保證多個變數操作的原子性,是做不到的。對於這種情況只能使用synchronized或者juc包中的Lock工具。

二、ABA問題

1、介紹

  程式碼示例:演示ABA問題

 1 // 原子引用類演示ABA問題
 2 public class ABATest {
 3     public static void main(String[] args) throws InterruptedException {
 4         AtomicReference<String> reference = new AtomicReference<>("A");
 5 
 6         // 執行緒 t1 由A修改B,又由B修改A
 7         new Thread(() -> {
 8             System.out.println(reference.compareAndSet("A", "B") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
 9             System.out.println(reference.compareAndSet("B", "A") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
10         }, "t1").start();
11 
12 
13         new Thread(() -> {
14             // 讓t1執行緒完成ABA操作
15             try {
16                 Thread.sleep(500);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20             System.out.println(reference.compareAndSet("A", "C") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
21 
22         }, "t2").start();
23 
24         Thread.sleep(1000);
25 
26         System.out.println(reference.get());
27     }
28 }
29 
30 // 結果
31 true. t1 value is:B
32 true. t1 value is:A
33 true. t2 value is:C
34 C

  如何理解ABA問題?
  可能你會覺得,執行緒 t2 不就是要將"A"改為"C"嘛,雖然中間變化了,但對 t2 也沒影響呀!
  比如:你的銀行卡里有10w,中間你領了工資1w,然後,又被扣除還了房貸1w,此時,你的銀行卡里還是10w。雖然結果沒變,但餘額已經不是原來的餘額了。而且,你一定在意中間你的錢去哪裡了,所以是不一樣的。
  再比如:對於公司財務來說,可能某一時刻,賬戶是100w,你偷偷挪用了公款20w,後來又悄悄補上了。雖然結果沒變,但中間的記賬明細,其實我們是關心的,因為這個時候你已經犯法了。

2、解決

  帶時間戳的原子引用:Java提供了AtomicStampedReference來解決ABA問題。其實其實就是加了版本號,每一次的修改,版本號都 +1。比對的是 記憶體值 + 版本號 是否一致。
  程式碼示例:解決ABA問題

 1 public class ABATest {
 2     public static void main(String[] args) throws InterruptedException {
 3 
 4         AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
 5         final int stamp = reference.getStamp();
 6 
 7         // 執行緒 t1 由A修改B,又由B修改A
 8         new Thread(() -> {
 9             System.out.println(reference.compareAndSet("A", "B", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
10             System.out.println(reference.compareAndSet("B", "A", reference.getStamp(), reference.getStamp() + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
11         }, "t1").start();
12         
13 
14         new Thread(() -> {
15             // 讓t1執行緒完成ABA操作
16             try {
17                 Thread.sleep(500);
18             } catch (InterruptedException e) {
19                 e.printStackTrace();
20             }
21             System.out.println(reference.compareAndSet("A", "C", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
22 
23         }, "t2").start();
24 
25         Thread.sleep(1000);
26 
27         System.out.println(reference.getReference());
28     }
29 }
30 
31 // 結果
32 true. t1 value is:B
33 true. t1 value is:A
34 false. t2 value is:A    // t2並沒有修改成功
35 A

  compareAndSet()方法的 4 個引數:

  expectedReference:表示期望的引用值
  newReference:表示要修改後的新引用值
  expectedStamp:表示期望的戳(版本號)
  newStamp:表示修改後新的戳(版本號)

3、原始碼分析

 1 public class AtomicStampedReference<V> {
 2 
 3     private static class Pair<T> {
 4         final T reference;
 5         final int stamp;
 6         private Pair(T reference, int stamp) {
 7             this.reference = reference;
 8             this.stamp = stamp;
 9         }
10         static <T> Pair<T> of(T reference, int stamp) {
11             return new Pair<T>(reference, stamp);
12         }
13     }
14     
15     public boolean compareAndSet(V   expectedReference,
16                                  V   newReference,
17                                  int expectedStamp,
18                                  int newStamp) {
19         Pair<V> current = pair;
20         return
21             expectedReference == current.reference &&
22             expectedStamp == current.stamp &&
23             ((newReference == current.reference &&
24               newStamp == current.stamp) ||
25              casPair(current, Pair.of(newReference, newStamp)));
26     }
27 
28     private boolean casPair(Pair<V> cmp, Pair<V> val) {
29         return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
30     }
31 }

  很簡單,維護了一對Pair,裡面除了引用reference,還有一個int型別的戳(版本號)。比較更新的時候,兩個變數都要比較。

三、LongAdder

1、介紹

  《阿里巴巴Java開發手冊》推薦使用LongAdder。

  AtomicLong,本質上是多個執行緒同時操作同一個目標資源,有且只有一個執行緒執行成功,其他執行緒都會失敗,不斷重試(自旋),自旋會成為瓶頸。
  而LongAdder的思想就是把要操作的目標資源[分散]到陣列Cell中,每個執行緒對自己的Cell變數的value進行原子操作,大大降低了失敗的次數。
  這就是為什麼在高併發場景下,推薦使用LongAdder的原因。

 

  參考文件:https://www.matools.com/api/java8

  《阿里巴巴Java開發手冊》百度網盤:https://pan.baidu.com/s/1aWT3v7Efq6wU3GgHOqm-CA 密碼: uxm8

相關文章