什么是CAS機制
CAS是英文單詞Compare and Swap的縮寫,翻譯過來就是比較並替換
CAS機制中使用了3個基本操作數:內存地址V,舊的預期值A,要修改的新值B。
看如下幾個例子:
package com.example.demo.concurrentDemo; import org.junit.Test; import java.util.concurrent.atomic.AtomicInteger; public class CasTest { private static int count = 0; @Test public void test1(){ for (int j = 0; j < 2; j++) { new Thread(() -> { for (int i = 0; i < 10000; i++) { count++; } }).start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //結果必定 count <= 20000 System.out.println(count); } @Test public void test2() { for (int j = 0; j < 2; j++) { new Thread(() -> { for (int i = 0; i < 10000; i++) { synchronized (this) { count++; } } }).start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //synchronized 類似於悲觀鎖 //synchronized關鍵字會讓沒有得到鎖資源的線程進入BLOCKED狀態,而后在爭奪到鎖資源后恢復為RUNNABLE狀態 //這個過程中涉及到操作系統用戶模式和內核模式的轉換,代價比較高 System.out.println(count); } private static AtomicInteger atoCount = new AtomicInteger(0); @Test public void test3() { for (int j = 0; j < 2; j++) { new Thread(() -> { for (int i = 0; i < 10000; i++) { atoCount.incrementAndGet(); } }).start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } //Atomic操作類的底層正是用到了“CAS機制” System.out.println(atoCount); } }
CAS 缺點
1) CPU開銷過大
在並發量比較高的情況下,如果許多線程反復嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。
這個可以通過看:AtomicInteger.incrementAndGet()源碼,可知這是一個無限循環,獲取實際值與預期值比較,當相等才會跳出循壞。
2) 不能保證代碼塊的原子性
CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。
3) ABA問題
這是CAS機制最大的問題所在。
什么是ABA?先看下面例子:
我們先來看一個多線程的運行場景:
時間點1 :線程1查詢值是否為A
時間點2 :線程2查詢值是否為A
時間點3 :線程2比較並更新值為B
時間點4 :線程2查詢值是否為B
時間點5 :線程2比較並更新值為A
時間點6 :線程1比較並更新值為C
在這個線程執行場景中,2個線程交替執行。線程1在時間點6的時候依然能夠正常的進行CAS操作,盡管在時間點2到時間點6期間已經發生一些意想不到的變化, 但是線程1對這些變化卻一無所知,因為對線程1來說A的確還在。通常將這類現象稱為ABA問題。
ABA發生了,但線程不知道。又或者鏈表的頭在變化了兩次后恢復了原值,但是不代表鏈表就沒有變化。
ABA隱患
就像兵法講的:偷梁換柱、李代桃僵
歷史事件:趙氏孤兒
解決ABA問題兩種方法:
1、悲觀鎖思路,加鎖;
2、樂觀鎖思路,通過AtomicStampedReference.class
源碼實現,具體看源碼:
1. 創建一個Pair類來記錄對象引用和時間戳信息,采用int作為時間戳,實際使用的時候時間戳信息要做成自增的,否則時間戳如果重復,還會出現ABA的問題。這個Pair對象是不可變對象,所有的屬性都是final的, of方法每次返回一個新的不可變對象。
2. 使用一個volatile類型的引用指向當前的Pair對象,一旦volatile引用發生變化,變化對所有線程可見。
3. set方法時,當要設置的對象和當前Pair對象不一樣時,新建一個不可變的Pair對象。
4. compareAndSet方法中,只有期望對象的引用和版本號和目標對象的引用和版本好都一樣時,才會新建一個Pair對象,然后用新建的Pair對象和原理的Pair對象做CAS操作。
5. 實際的CAS操作比較的是當前的pair對象和新建的pair對象,pair對象封裝了引用和時間戳信息。
Demo:
@Test public void test4() { final int timeStamp = atoReferenceCount.getStamp(); new Thread(() -> { while(true){ if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(), atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){ System.out.println("11111111"); break; } } },"線程1:").start(); new Thread(() -> { while(true){ if(atoReferenceCount.compareAndSet(atoReferenceCount.getReference(), atoReferenceCount.getReference()+1, timeStamp, timeStamp + 1)){ System.out.println("2222222"); break; } } },"線程2:").start(); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atoReferenceCount.getReference()); }
第二個沒有執行,因為時間戳不對了。
修改下代碼:
@Test public void test4() { for (int i = 0; i < 10; i++) { new Thread(() -> { boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(), atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(), atoReferenceCount.getStamp() + 1); System.out.println("線程"+Thread.currentThread()+"result="+f); }, "線程:"+i).start(); } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atoReferenceCount.getReference()); }
結果:可見線程:0,比較的時候發現時間戳變了,所以沒有+1。
demo2:
@Test public void test5() { for (int i = 0; i < 4; i++) { new Thread(() -> { for (int j = 0; j < 500; j++) { boolean f = atoReferenceCount.compareAndSet(atoReferenceCount.getReference(), atoReferenceCount.getReference() + 1, atoReferenceCount.getStamp(), atoReferenceCount.getStamp() + 1); System.out.println("線程"+Thread.currentThread()+">>j="+j+",result="+f); } }, "線程:"+i).start(); } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atoReferenceCount.getReference()); }
有3次比較時間戳發現已經不同
參考:
https://blog.csdn.net/qq_32998153/article/details/79529704