一、原子類
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