深入理解CAS
前言
首先這篇文章是對前文深入理解ConcurrentHashMap中提到的CAS概念做補充的。其次是講解CAS理論,我也看過很多關於CAS的部落格,重複性,概念性都太強了,我要做的與眾不同,我會把我所理解的用通俗易懂的語言描述出來的。
正文
什麼是CAS
CAS(比較與交換,Compare and swap)是一種有名的無鎖演算法。
CAS工作原理
CAS指令需要有3個運算元,分別是記憶體為止(在Java中可以簡單理解為變數的記憶體地址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,當且僅當V符合舊預期值A時,處理器用新值B更新V的值,否則他就不執行更新,但是無論是否更新了V的值,都會返回V的舊值,上述的處理過程是一個原子操作。
如何使用CAS操作來避免阻塞同步
下面的程式碼主要是使用了20個執行緒進行自增10000次來證明原子性.執行結果是:20000
public static AtomicInteger race = new AtomicInteger(0); private static final int THREADS_COUNT = 20; public static void increase() { race.incrementAndGet(); } @Test public void atomicTest() { Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) Thread.yield(); System.out.println(race); }
我們使用了AtomicInteger了,程式輸出正確結果,一切都要歸功於incrementAndGet()方法的原子性,該方法無限迴圈,不斷嘗試將一個一個比當前值大1的新值賦給自己,如果失敗了那說明在執行“獲取-設定“操作的時候值已經有了修改,於是再次迴圈進行下一次操作,只帶設定成功為止,它的原理實現其實非常簡單。程式碼如下:
/** * Atomically increments by one the current value. * * @return the updated value */ 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.class 大家可以自己進去看一看
CAS缺點
如果一個變數V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說它值沒有被其他執行緒改變過嗎?
如果在這段期間它的值曾經改成了B,後來又改成了A,那麼CAS操作就會誤認為它沒有改變過,這個漏洞稱為“ABA”問題。J.U.C包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變數值的版本來保證CAS的正確性,如果需要解決ABA問題,改用傳統的互斥同步(典型的就是synchronized 和Lock)可能會比原子類更高效。
總結:Unsafe類是CAS實現的核心。 從名字可知,這個類標記為不安全的,CAS會使得程式設計比較負責,但是由於其優越的效能優勢,以及天生免疫死鎖(根本就沒有鎖,當然就不會有執行緒一直阻塞了),更為重要的是,使用無鎖的方式沒有所競爭帶來的開銷,也沒有執行緒間頻繁排程帶來的開銷,他比基於鎖的方式有更優越的效能,所以在目前被廣泛應用,我們在程式設計時也可以適當的使用.不過由於CAS編碼確實稍微複雜,而且jdk作者本身也不希望你直接使用unsafe,所以如果不能深刻理解CAS以及unsafe還是要慎用,使用一些別人已經實現好的無鎖類或者框架就好了。
附:
JVM中的CAS
堆中物件的分配
簡單的說new出來一個物件之前大小其實已經固定,把他放到堆裡以什麼形式儲存的呢?
由於再給一個物件分配記憶體的時候不是原子性的操作,至少需要以下幾步:查詢空閒列表、分配記憶體、修改空閒列表等等,這是不安全的。解決併發時的安全問題也有兩種策略:
- CAS
實際上虛擬機器採用CAS配合上失敗重試的方式保證更新操作的原子性,原理和上面講的一樣。
- TLAB
如果使用CAS其實對效能還是會有影響的,所以JVM又提出了一種更高階的優化策略:每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝區(TLAB),執行緒內部需要分配記憶體時直接在TLAB上分配就行,避免了執行緒衝突。只有當緩衝區的記憶體用光需要重新分配記憶體的時候才會進行CAS操作分配更大的記憶體空間。
虛擬機器是否使用TLAB,可以通過-XX:+/-UseTLAB引數來進行配置(jdk5及以後的版本預設是啟用TLAB的)。
注:對本文有異議或不明白的地方微信探討,wx:15524579896