CAS (Compare and Swap)
CAS字面意思爲比較並交換.CAS 有 3 個操做數,分別是:內存值 M,指望值 E,更新值 U。當且僅當內存值 M 和指望值 E 相等時,將內存值 M 修改成 U,不然什麼都不作。java
1.CAS的應用場景
CAS 只適用於線程衝突較少的狀況。算法
CAS 的典型應用場景是:安全
- 原子類
- 自旋鎖
1.1 原子類
原子類是 CAS 在 Java 中最典型的應用。併發
咱們先來看一個常見的代碼片斷。ide
if(a==b) { a++; }
若是 a++
執行前, a 的值被修改了怎麼辦?還能獲得預期值嗎?出現該問題的緣由是在併發環境下,以上代碼片斷不是原子操做,隨時可能被其餘線程所篡改。性能
解決這種問題的最經典方式是應用原子類的 incrementAndGet
方法。this
public class AtomicIntegerDemo { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(3); final AtomicInteger count = new AtomicInteger(0); for (int i = 0; i < 10; i++) { executorService.execute(new Runnable() { @Override public void run() { count.incrementAndGet(); } }); } executorService.shutdown(); executorService.awaitTermination(3, TimeUnit.SECONDS); System.out.println("Final Count is : " + count.get()); } }
J.U.C 包中提供了 AtomicBoolean
、AtomicInteger
、AtomicLong
分別針對 Boolean
、Integer
、Long
執行原子操做,操做和上面的示例大致類似,不作贅述。atom
1.2 自旋鎖
利用原子類(本質上是 CAS),能夠實現自旋鎖。操作系統
所謂自旋鎖,是指線程反覆檢查鎖變量是否可用,直到成功爲止。因爲線程在這一過程當中保持執行,所以是一種忙等待。一旦獲取了自旋鎖,線程會一直保持該鎖,直至顯式釋放自旋鎖。線程
示例:非線程安全示例
public class AtomicReferenceDemo { private static int ticket = 10; public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.execute(new MyThread()); } executorService.shutdown(); } static class MyThread implements Runnable { @Override public void run() { while (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票"); ticket--; } } } }
輸出結果:
pool-1-thread-2 賣出了第 10 張票 pool-1-thread-1 賣出了第 10 張票 pool-1-thread-3 賣出了第 10 張票 pool-1-thread-1 賣出了第 8 張票 pool-1-thread-2 賣出了第 9 張票 pool-1-thread-1 賣出了第 6 張票 pool-1-thread-3 賣出了第 7 張票 pool-1-thread-1 賣出了第 4 張票 pool-1-thread-2 賣出了第 5 張票 pool-1-thread-1 賣出了第 2 張票 pool-1-thread-3 賣出了第 3 張票 pool-1-thread-2 賣出了第 1 張票
很明顯,出現了重複售票的狀況。
【示例】使用自旋鎖來保證線程安全
能夠經過自旋鎖這種非阻塞同步來保證線程安全,下面使用 AtomicReference
來實現一個自旋鎖。
public class AtomicReferenceDemo2 { private static int ticket = 10; public static void main(String[] args) { threadSafeDemo(); } private static void threadSafeDemo() { SpinLock lock = new SpinLock(); ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executorService.execute(new MyThread(lock)); } executorService.shutdown(); } static class SpinLock { private AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void lock() { Thread current = Thread.currentThread(); while (!atomicReference.compareAndSet(null, current)) {} } public void unlock() { Thread current = Thread.currentThread(); atomicReference.compareAndSet(current, null); } } static class MyThread implements Runnable { private SpinLock lock; public MyThread(SpinLock lock) { this.lock = lock; } @Override public void run() { while (ticket > 0) { lock.lock(); if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 賣出了第 " + ticket + " 張票"); ticket--; } lock.unlock(); } } } }
輸出結果:
pool-1-thread-2 賣出了第 10 張票 pool-1-thread-1 賣出了第 9 張票 pool-1-thread-3 賣出了第 8 張票 pool-1-thread-2 賣出了第 7 張票 pool-1-thread-3 賣出了第 6 張票 pool-1-thread-1 賣出了第 5 張票 pool-1-thread-2 賣出了第 4 張票 pool-1-thread-1 賣出了第 3 張票 pool-1-thread-3 賣出了第 2 張票 pool-1-thread-1 賣出了第 1 張票
2.CAS 的原理
Java 主要利用 Unsafe
這個類提供的 CAS 操做。Unsafe
的 CAS 依賴的是 JVM 針對不一樣的操做系統實現的硬件指令 Atomic::cmpxchg
。Atomic::cmpxchg
的實現使用了彙編的 CAS 操做,並使用 CPU 提供的 lock
信號保證其原子性。
3.CAS 帶來的問題
通常狀況下,CAS 比鎖性能更高。由於 CAS 是一種非阻塞算法,因此其避免了線程阻塞和喚醒的等待時間。
可是,事物總會有利有弊,CAS 也存在三大問題:
ABA 問題
循環時間長開銷大
只能保證一個共享變量的原子性
如何解決這三個問題:
3.1 ABA 問題
若是一個變量初次讀取的時候是 A 值,它的值被改爲了 B,後來又被改回爲 A,那 CAS 操做就會誤認爲它歷來沒有被改變過。
J.U.C 包提供了一個帶有標記的原子引用類 如:AtomicStampedReference
來解決這個問題,它能夠經過控制變量值的版原本保證 CAS 的正確性。大部分狀況下 ABA 問題不會影響程序併發的正確性,若是須要解決 ABA 問題,改用傳統的互斥同步可能會比原子類更高效。 解決方案:增長標誌位,例如:AtomicMarkableReference、AtomicStampedReference
3.2 循環時間長開銷大
自旋 CAS (不斷嘗試,直到成功爲止)若是長時間不成功,會給 CPU 帶來很是大的執行開銷。
若是 JVM 能支持處理器提供的 pause
指令那麼效率會有必定的提高,pause
指令有兩個做用:
- 它能夠延遲流水線執行指令(de-pipeline),使 CPU 不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。
- 它能夠避免在退出循環的時候因內存順序衝突(memory order violation)而引發 CPU 流水線被清空(CPU pipeline flush),從而提升 CPU 的執行效率。
解決方案:由於是while循環,消耗必然大。設置嘗試次數上限
3.3只能保證一個共享變量的原子性
當對一個共享變量執行操做時,咱們可使用循環 CAS 的方式來保證原子操做,可是對多個共享變量操做時,循環 CAS 就沒法保證操做的原子性,這個時候就能夠用鎖。
或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比有兩個共享變量 i = 2, j = a
,合併一下 ij=2a
,而後用 CAS 來操做 ij
。從 Java 1.5 開始 JDK 提供了 AtomicReference
類來保證引用對象之間的原子性 解決方案:用AtomicReference把多個變量封裝成一個對象來進行CAS操做.
關注公衆號:java寶典