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