Java深入学习30:CAS中的ABA问题以及解决方案

 什么是ABA问题

  在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。这个时间差中,会导致数据的变化。

  假设如下事件序列:

  1. 线程 1 从内存位置V中取出A。
  2. 线程 2 从位置V中取出A。
  3. 线程 2 进行了一些操作,将B写入位置V。
  4. 线程 2 将A再次写入位置V。
  5. 线程 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

posted on 2020-07-09 16:00  我不吃番茄  阅读(780)  评论(0编辑  收藏  举报