CAS 即 compare and swap 比較並交換, 涉及到三個參數,內存值V, 預期值A, 要更新爲的值B, 拿着預期值A與內存值V比較,相等則符合預期,將內存值V更新爲B, 不相等,則不能更新V。java
爲何預期值A與內存值V不同了呢?數據庫
在多線程環境下,對於臨界區的共享資源,全部線程均可以訪問修改,這時爲了保證數據不會發生錯誤,一般會對訪問臨界區資源加鎖,同一時刻最多隻能讓一個線程訪問(獨佔模式下),這樣會讓線程到臨界區時串行執行,加鎖操做可能會致使併發性能下降,而循環CAS能夠實現讓多個線程不加鎖去訪問共享資源,卻也能夠保證數據正確性。 如 int share = 1,線程A獲取到share的值1,想要將其修改成2,這時線程B搶先修改share = 3了,線程A這時拿着share =1 預期值與實際內存中已經變爲3的值比較, 不相等,cas失敗,這時就從新獲取最新的share再次更新,須要不斷循環,直到更新成功;這裏可能會存在線程一直在進行循環cas,消耗cpu資源。安全
cas缺點:多線程
一、存在ABA問題併發
二、循環cas, 可能會花費大量時間在循環,浪費cpu資源ide
三、只能更新一個值(也可解決,AtomicReference
原子類在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本數據類型原子類,還有可傳泛型的AtomicReference
類圖結構以下:this
分別用AtomicInteger和 Integer 演示多個線程執行自增操做,是否可以保證原子性,執行結果是否正確atom
代碼以下:
/** * @author zdd * 2019/12/22 10:47 上午 * Description: 演示AtomicInteger原子類原子操做 */ public class CasAtomicIntegerTest { static final Integer THREAD_NUMBER = 10; static AtomicInteger atomicInteger = new AtomicInteger(0); static volatile Integer integer = 0; public static void main(String[] args) throws InterruptedException { ThreadTask task = new ThreadTask(); Thread[] threads = new Thread[THREAD_NUMBER]; //1,開啓10個線程 for (int j = 0; j < THREAD_NUMBER; j++) { Thread thread = new Thread(task); threads[j]= thread; } for (Thread thread:threads) { //開啓線程 thread.start(); //注: join 爲了保證主線程在全部子線程執行完畢後再打印結果,不然主線程就阻塞等待 // thread.join(); } // 主線程休眠5s, 等待全部子線程執行完畢再打印 TimeUnit.SECONDS.sleep(5); System.out.println("執行完畢,atomicInteger的值爲: "+ atomicInteger.get()); System.out.println("執行完畢,integer的值爲 : "+ integer); } public static void safeIncr() { atomicInteger.incrementAndGet(); } public static void unSafeIncr() { integer ++; } static class ThreadTask implements Runnable{ @Override public void run() { // 任務體,分別安全和非安全方式自增1000次 for (int i = 0; i < 1000; i++) { safeIncr(); } for (int i = 0; i < 1000; i++) { unSafeIncr(); } } } }
執行結果以下:
疑問:上文代碼中注,我本想讓主線程調用每一個子線程 join方法,保證主線程在全部子線程執行完畢以後再執行打印結果,然而這樣執行致使非安全的Integer自增結果也正確,猜測是在執行join方法,致使這10個子線程排隊有序在執行了? 所以註釋了該行代碼 ,改成讓主線程休眠幾秒來保證在子線程執行後再打印。
AtomicInteger如何保證原子性,AtomicInteger持有Unsafe對象,其大部分方法是本地方法,底層實現可保證原子操做。
public class AtomicInteger extends Number implements java.io.Serializable { // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe();
來看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增長後的值。
代碼以下:
public final int incrementAndGet() { //調用unsafe的方法 return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
繼續看unsafe如何實現
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //1.獲取當前對象的內存中的值A var5 = this.getIntVolatile(var1, var2); //2. var1,var2聯合獲取內存中的值V,var5是指望中的值A, var5+var4 是將要更新爲的新值 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //3. 更新成功,跳出while循環,返回更新成功時內存中的值(可能下一刻就被其餘線程修改) return var5; }
執行流程圖以下:
Unsafe 的compareAndSwapInt是本地方法,可原子地執行更新操做,更新成功返回true,不然false
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
什麼是ABA問題?
例如 線程A獲取變量atomicInteger =100, 想要將其修改成2019 (此時還未修改), 這時線程B搶先進來將atomicInteger先修改成101,再修改回atomicInteger =100,這時線程A開始去更新atomicInteger的值了,此時預期值和內存值相等,更新成功atomicInteger =2019;可是線程A 並不知道這個值其實已經被人修改過了。
代碼演示以下:
/** * zdd * Description: cas的ABA問題 */ public class CasTest1 { // static AtomicInteger atomicInteger = new AtomicInteger(100); /* 這裏使用原子引用類,傳入Integer類型, * 和AtomicInteger同樣,AtomicReference使用更靈活,泛型可指定任何引用類型。 * 也可用上面註釋代碼 */ static AtomicReference<Integer> reference = new AtomicReference<>(100); public static void main(String[] args) { //1.開啓線程A new Thread(()-> { Integer expect = reference.get(); try { //模擬執行任務,讓線程B搶先修改 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( "執行3s任務後, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ " 當前值爲: "+ reference.get()); },"A").start(); //2.開啓線程B new Thread(()-> { // expect1 =100 Integer expect1 = reference.get(); //1,先修改成101,再修改回100,產生ABA問題 reference.compareAndSet(expect1,101); //expect2 =101 Integer expect2 = reference.get(); reference.compareAndSet(expect2, 100); },"B").start(); } }
執行結果以下:可見線程A修改爲功
A 執行3s任務後, 修改值是否成功:true 當前值爲: 2019
解決CAS的ABA問題,是參照數據庫樂觀鎖,添加一個版本號,每更新一次,次數+1,就可解決ABA問題了。
AtomicStampedReference
/** * zdd * 2019/11/4 6:30 下午 * Description: */ public class CasTest1 { //設置初始值和版本號 static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1); public static void main(String[] args) { //2,採用帶有版本號的 new Thread(()-> { Integer expect = stampedReference.getReference(); int stamp = stampedReference.getStamp(); try { //休眠3s,讓線程B執行完ABA操做 TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } //此時 stamp=1,與實際版本號3不等,這裏更新失敗就是stamp沒有獲取到最新的 System.out.println("是否修改爲功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1)); System.out.println("當前 stamp 值: " + stampedReference.getStamp()+ "當前 reference: " +stampedReference.getReference()); },"A").start(); new Thread(()-> { Integer expect = stampedReference.getReference(); int stamp = stampedReference.getStamp(); try { //休眠1s,讓線程A獲取都舊的值和版本號 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } // 1,100 -> 101, 版本號 1-> 2 stampedReference.compareAndSet(expect, 101 , stamp, stamp+1); //2, 101 ->100, 版本號 2->3 Integer expect2 = stampedReference.getReference(); stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1); },"B").start(); } }
執行結果以下:
是否修改爲功: false 當前 stamp 值: 3 當前 reference: 100
package cas; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * @author zdd * 2019/12/22 9:12 下午 * Description: 利用cas手動實現自旋鎖 */ public class SpinLockTest { static AtomicReference<Thread> atomicReference = new AtomicReference<>(); public static void main(String[] args) { SpinLockTest spinLockTest = new SpinLockTest(); //測試使用自旋鎖,達到同步鎖同樣的效果 ,開啓2個子線程 new Thread(()-> { spinLockTest.lock(); System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis()); try { //休眠3s TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis()); spinLockTest.unLock(); },"線程A").start(); new Thread(()-> { spinLockTest.lock(); System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis()); try { //休眠3s TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis()); spinLockTest.unLock(); },"線程B").start(); } public static void lock() { Thread currentThread = Thread.currentThread(); for (;;) { boolean flag =atomicReference.compareAndSet(null,currentThread); //cas更新成功,則跳出循環,不然一直輪詢 if(flag) { break; } } } public static void unLock() { Thread currentThread = Thread.currentThread(); Thread momeryThread = atomicReference.get(); //比較內存中線程對象與當前對象,不等拋出異常,防止未獲取到鎖的線程調用unlock if(currentThread != momeryThread) { throw new IllegalMonitorStateException(); } //釋放鎖 atomicReference.compareAndSet(currentThread,null); } }
執行結果以下圖:
經過全文,咱們能夠知道cas的概念,它的優缺點;原子類的使用,內部藉助Unsafe類循環cas更新操做實現無鎖狀況下保證原子更新操做,進一步咱們可以本身利用循環cas實現自旋鎖SpinLock,它與同步鎖如ReentrantLock等區別在於自旋鎖是在未獲取到鎖狀況,一直在輪詢,線程時非阻塞的,對cpu資源佔用大,適合查詢多修改少場景,併發性能高;同步鎖是未獲取到鎖,阻塞等待,二者各有適用場景。