【JUC】CAS(Compare And Swap)及其ABA問題

CAS和AtomicInteger

AtomicInteger用來保證自增原子性,它的實現是基於CAS(比較和交換)的。

CAS(CompareAndSwap):判斷內存某個位置的值是否與預期值一致,如果是則更改爲新值,這個過程是原子的。不會造成數據不一致的問題。
compareAndSet(except, update)方法: except是操作數據前從主內存中拿到的值,update是在工作內存中對變量拷貝副本進行修改的值,如果修改值之後,主內存的值還沒有被改過,也就是except裏的值,則可以將該值更新爲update,並返回true。否則,返回false。

CAS的底層原理

自旋鎖

上述代碼:
  1. var1 AtomicInteger本身也就是this
  2. var2 該對象值的引用地址也就是valueOffset
  3. var4 需要變動的數量+X
  4. var5 用var1+var2找出來的真實的值
  5. 比較真實的值var5和對象當前的值var2 成功則var5 += var 4,並退出循環,
  6. 否則重新獲取值,再比較。直到成功,退出循環。
【自旋鎖的原理:自己循環比較直到相同】
線程A和B分別拷貝主內存中的值到自己的工作內存中,打算進行+1操作。
線程A通過getIntVolatile拿到value值爲3,線程A被掛起。
線程B通過getIntVolatile拿到value值爲3,線程B執行compareAndSwapInt方法比較內存值也爲3,成功修改內存值爲4,線程B任務完成。
線程A被喚起,執行compareAndSwapInt方法自己的值3和內存值4(volitale修改的可見性)比較,不一致,說明這個數據已經被修改了,線程A任務失敗,線程A重新通過getIntVolatile拿到value值爲4,線程A執行compareAndSwapInt方法比較內存值也爲4,成功修改內存值爲5。

 

UnSafe類

Unsafe類是CAS的核心類。Java方法無法直接訪問底層系統,需要通過本地native方法來訪問。Unsafe方法相當於一個後門,基於該類可以直接操作特定內存的數據。Unsafe類在rt.jar/sun.misc包中,內部方法可以像C的指針一樣直接操作內存。valueOffset存放內存中的偏移地址,Unsafe通過內存偏移地址來獲取數據。
調用Unsafe類中的CAS方法,JVM會實現CAS彙編指令,實現原子操作,原語的執行必須是連續的,在執行過程中不允許被打斷。CAS是CPU的原子指令,不會造成所謂的數據不一致問題。【Atomic::cmpxchg(x,addr,e)==e】
 
爲什麼不用synchronizated:因爲他加鎖之後只能讓一個線程訪問,雖然一致性得到了保證,但是併發性下降。

CAS的缺點是什麼?

  1. CAS基於自旋鎖實現,它需要多次比較,循環時間長(比較成功才退出循環,否則一致循環),CPU開銷大
  2. 只能保證1個共享變量的操作。多個共享變量就無法保證操作的原子性,只能通過加鎖來保證。【Unsafe類的getAndAddInt傳入的對象是this,只有一個對象】
  3. 存在ABA問題

什麼是ABA問題?

CAS算法實現的前提是取出內存中某時刻的數據並在當下時刻比較並替換,這個時間差會導致數據的變化。
 
例如:線程1從內存中取出A,線程2也取出A,線程2操作後將值變成B,然後寫回內存,然後又讀取B將數據改成A,再寫回內存。線程1進行CAS操作的時候發現內存中還是A,然後線程1操作成功。【內存中的值曾經變化過,但是線程1不知道】
 
CAS是一種樂觀鎖,它只關心頭和尾,不關心過程中的變化情況。

ABA問題怎麼解決?

  1. 使用AtomicReference原子引用來解決。
  2. AtomicReference增加一種機制,就是修改版本號,類似於時間戳。

static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);

參數:初始化值、版本號。

flag=stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);

參數:期望的值、更新的值、期望的版本號、更新的版本號。

只有值和版本號都對應的時候纔可以修改成功。
 

ABA問題的危害是什麼?

在許多業務場景下,ABA問題並不會造成什麼危害。
實際上在某些問題中,ABA問題也會造成數據不一致的問題。
比如在棧中從棧頂到棧底存放着ABCD四個元素。
線程1想要在棧頂爲A的情況下,壓入E,得到最後棧中從棧頂到棧底存放的是EBCD。
線程1首先查詢棧頂是否爲A,線程1獲取到棧頂的值之後,被線程2中斷。
線程2將ABCD依次彈出後,再壓入A,此時棧中只剩下一個A。
線程1被喚醒,獲取當前棧頂的值,仍然是A和之前獲取到的值是一致的,所以可以操作,彈出A後壓入E。
此時棧中僅僅只有E,而不是線程1想要的EBCD。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章