概述
cas即(compare and swap),比較並交換,在java並發中使用非常廣泛,無論是ReenterLock內部的AQS,還是各種Atomic開頭的原子類,都是基於cas實現的,java8的ConcurrentHashMap也使用了cas + synchronized進行實現,本文就介紹一下cas的原理。
cas原理
在CAS中有三個參數:內存值V、舊的預期值A、要更新的值B,當且僅當內存值V的值等於舊的預期值A時才會將內存值V的值修改為B,否則什么都不干。以上過程整個是一個原子操作。
硬件底層實現原子性的方法:
- 總線鎖定:當某個CPU需要修改某個數據的時候,通過鎖住內存總線,使得別的CPU無法訪問內存中的數據,從而保證緩存的一致性,但這種實現方式會導致CPU執行效率降低,現在很少被使用。
- 緩存鎖:當一個CPU要修改緩存中的變量時,會對緩存加鎖,同時會通過總線通知別的CPU,讓他們的變量副本失效,這樣同樣可以保證一次只有一個CPU修改變量的值,從而保證緩存一致性。
AtomicInteger如何使用cas保證原子性
下面分析一下AtomicInteger源碼
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
解釋如下:
valueOffset:這個變量的作用是為了記錄value的內存地址的,在AtomicInteger初始化的時候,會執行static{}這段靜態代碼塊,之后會給這個變量賦值,通過unsafe的objectFieldOffset方法獲取value的地址,有了這個內存地址,就可以使用CAS對value進行原子操作。
Unsafe是CAS的核心類,Java無法直接訪問底層操作系統,而是通過本地(native)方法來訪問。不過盡管如此,JVM還是開了一個后門:Unsafe,它提供了硬件級別的原子操作。
下面來看下AtomicInteger的#addAndGet方法
public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
進入#unsafe.getAndAddInt()
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; }
這里有一個while循環會一致嘗試,直到cas成功,這就是cas的一個缺點,忙等,下面還會介紹。
看一下#compareAndSwapInt(),這個就是實現CAS操作的方法,是一個native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
參數的含義:當前對象、value的地址、預期值、修改值
下面用一個圖演示一下上面代碼的過程
圖片來源:Java魔法類:Unsafe應用解析
上圖為某個AtomicInteger對象自增操作前后的內存示意圖,對象的基地址baseAddress=“0x110000”,通過baseAddress+valueOffset得到value的內存地址valueAddress=“0x11000c”;然后通過CAS進行原子性的更新操作,成功則返回,否則繼續重試,直到更新成功為止。
CAS的缺點
循環時間太長
如果CAS一直不成功呢?這種情況絕對有可能發生,如果自旋CAS長時間地不成功,則會給CPU帶來非常大的開銷。在JUC中有些地方就限制了CAS自旋的次數,例如BlockingQueue的SynchronousQueue。
ABA問題
CAS需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是A,變成了B,然后又變成了A,那么在CAS檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。
Java中提供了AtomicStampedReference來解決,AtomicStampedReference通過包裝一個元組[E,Integer],其中第一個元素就是要修改的元素,第二個就是為了記錄版本信息,使用一個類來封裝這兩個變量信息,類源碼如下:
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair;
該類是AtomicStampedReference中的一個靜態內部類,第一個參數reference就是要修改的變量信息,第二個stamp就是版本信息,而且這兩個變量都被final修飾,也就是說這個對象一旦初始化,這兩個對象的值就確定了,不可以更改,那也就是說每次執行CAS成功,都要重新創建一個新的Pair對象。
下面看一下AtomicStampedReference的#compareAndSet方法
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
參數解釋:預期引用(cas要修改的原值)、更新后的引用(cas修改之后的值)、預期標志(版本)、更新后的標志(版本)。后面是一個return,這個里面的代碼比較長,前面兩個判斷是為了看一下現在系統中的值和版本是不是和自己期望的一致,如果一致,進行下一個判斷,如果更新后的值和版本和系統當前的值和版本一致,直接返回,意思就是沒有必要執行CAS操作,因為自己更新后的和更新前的版本和值都是一樣,傳入的參數有問題,如果不一樣,則會進行CAS更新,會重新新建一個Pair對象,Pair.of這個方法在上面Pair源碼中有貼出,大家參考着看就行了。
總結
本文介紹CAS的原理,以及分析了Java中的AtomicInteger對象是如何使用CAS進行原子操作的,之后分析了CAS存在的一些問題,以及這些問題的解決辦法,總的來說CAS並不復雜,只是需要調用Unsafe,其實這個魔法類還是挺復雜的。
參考: