Java深入學習30:CAS中的ABA問題以及解決方案
什么是ABA問題
在CAS算法中,需要取出內存中某時刻的數據(由用戶完成),在下一時刻比較並替換(由CPU完成,該操作是原子的)。這個時間差中,會導致數據的變化。
假設如下事件序列:
- 線程 1 從內存位置V中取出A。
- 線程 2 從位置V中取出A。
- 線程 2 進行了一些操作,將B寫入位置V。
- 線程 2 將A再次寫入位置V。
- 線程 1 進行CAS操作,發現位置V中仍然是A,操作成功。
盡管線程 1 的CAS操作成功,但不代表這個過程沒有問題——對於線程 1 ,線程 2 的修改已經丟失;我們不能忽略線程2對數據的兩次修改
代碼模擬ABA問題
線程1,對數據100進行了兩次操作,先將100改成101,再將101改回100;線程2直接將100該成2020;雖然線程2修改成功了,但是在線程2修改之前,線程1已經對100進行了兩次操作。線程2修改的100並不是原來的那個100了;
public class ABATest { public static void main(String[] args) { AtomicInteger at = new AtomicInteger(100); new Thread(()->{ System.out.println(Thread.currentThread().getName() + "\t" + at.compareAndSet(100,101) + "\t num = " + at.get()); System.out.println(Thread.currentThread().getName() + "\t" + at.compareAndSet(101,100) + "\t num = " + at.get()); },"thread1").start(); new Thread(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t" + at.compareAndSet(100,2020) + "\t num = " + at.get()); },"thread2").start(); } } 日志 thread1 true num = 101 thread1 true num = 100 thread2 true num = 2020
如何規避ABA問題
使用AtomicStampedReference類,簡單說AtomicStampedReference類引入了版本概念(類似數據庫使用版本號進行樂觀鎖),每次進行compareAndSet操作是都進行版本好的迭代,只有當同時滿足CAS的(1)期望值正確匹配(2)版本號正確匹配,才能正確compareAndSet。
如下示例,線程2中的 compareAndSet 因為版本匹配錯誤而返回 flase;
public class ABASolvedTest { public static void main(String[] args) { AtomicStampedReference<Integer> asr = new AtomicStampedReference(100,1); new Thread(()->{ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t第1次版本號" + asr.getStamp() + "\t 當前值" + asr.getReference()); asr.compareAndSet(100,101,asr.getStamp(),asr.getStamp()+1); System.out.println(Thread.currentThread().getName() + "\t第2次版本號" + asr.getStamp() + "\t 當前值" + asr.getReference()); asr.compareAndSet(101,100,asr.getStamp(),asr.getStamp()+1); System.out.println(Thread.currentThread().getName() + "\t第3次版本號" + asr.getStamp() + "\t 當前值" + asr.getReference()); },"thread1").start(); new Thread(()->{ int stamp = asr.getStamp(); System.out.println(Thread.currentThread().getName() + "\t第1次版本號" +stamp); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = asr.compareAndSet(100, 2019, stamp, stamp + 1); System.out.println(Thread.currentThread().getName() + "\t是否更新成功 " + b); System.out.println(Thread.currentThread().getName() + "\t更新后的版本號" + asr.getStamp()); System.out.println(Thread.currentThread().getName() + "\t更新后的值" + asr.getReference()); },"thread2").start(); } } 日志 thread2 第1次版本號1 thread1 第1次版本號1 當前值100 thread1 第2次版本號2 當前值101 thread1 第3次版本號3 當前值100 thread2 是否更新成功 false thread2 更新后的版本號3 thread2 更新后的值100
END
