ABA问题

上一篇写CAS的时候,我列举了它的三个缺点,我们先来复习一下:

CAS的缺点:

  • 循环时间长,开销大(主要原因:do{}while()可能无限循环)

  • 只能保证一个共享变量的原子操作,如果是多个共享变量的话,就需要加锁来保证原子性。

  • 会引出ABA问题。

 

那么这一篇我们来说一下什么是ABA问题

 

ABA问题(狸猫换太子):

假设现在有T1和T2两个线程,T1线程的执行时间为10秒,T2线程的执行时间为2秒。程序执行时,T1和T2都从主物理内存中拷贝一份值到自己的工作内存,由于T2线程的执行速度太快,T2先将A改为了B,然后写回主物理内存,之后又将B改为A,再次写回主物理内存,这个时候T1线程是不知道的,因为它的执行速度太慢。那么在T1线程执行完毕后,它会判断主物理内存中的值和自己的期望值是不是一样的,它发现A=A,是一样的,所以T1认为没有人修改过主物理内存中的值,它会继续它的操作。但事实上,中间经过了ABA的一种变化,只是T1不知道而已,这就是所谓的ABA问题。

 

CAS引出的ABA问题:CAS只管开头和结尾,如果头尾一致,它就认为没人修改过,但实际上,中间可能被人狸猫换太子了。

 

那么其实ABA问题也是基于原子引用的,所以我们先来说一下原子引用。

原子引用:

将一个java类用原子引用类包装起来,这个类就具备了原子性。

代码示例:

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("z3",22);
        User li4 = new User("li4",25);
​
        AtomicReference<User> atomicReference = new AtomicReference<>();
        //设置主物理内存的共享变量是z3
        atomicReference.set(z3);
​
        System.out.println(atomicReference.compareAndSet(z3,li4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3,li4) + "\t" + atomicReference.get().toString());
    }
​
}
​
class User{
    String userName;
    int age;
​
    @Override
    public String toString() {
        return "userName:"+userName + "\tage:" + age;
    }
​
    public String getUserName() {
        return userName;
    }
    public int getAge() {
        return age;
    }
    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }
}

 

运行结果:

 

基于原子应用的ABA问题:

public class ABADemo {    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);    public static void main(String[] args) {        new Thread(()->{            atomicReference.compareAndSet(100,101);            atomicReference.compareAndSet(101,100);        },"t1").start();        new Thread(()->{            //让t2休眠1秒,保证t1线程完成ABA操作            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }            atomicReference.compareAndSet(100, 2020);            System.out.println(Thread.currentThread().getName() + "\t"+atomicReference.get());        },"t2").start();    }}

运行结果:

t2线程修改成功了,这就是ABA问题。

 

解决ABA问题:

怎么解决ABA问题呢?可以通过修改版本号的方式解决ABA问题。

这种方式类似于时间戳的概念。

 

AtomicStampedReference

时间戳的原子引用,每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号。

 

代码示例:

public class ABADemo {  //ABA问题的解决    AtomicStampedReference
​
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
​
    public static void main(String[] args) {
​
        System.out.println("==================以下是ABA问题的产生======================");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();
​
        new Thread(()->{
            try {
                //暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
        },"t2").start();
​
        //暂停一会线程
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        System.out.println("===============以下是ABA问题的解决========================");
​
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:"+stamp);
            //暂停1秒钟t3线程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本号:"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号:"+atomicStampedReference.getStamp());
        },"t3").start();
​
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号:"+stamp);
            //暂停3秒钟t4线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
​
            boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改成功否:"+result+"\t当前最新实际版本号:"+atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:"+atomicStampedReference.getReference());
        },"t4").start();
​
    }
}

 

运行结果:

我们可以发现,在加了版本号之后,ABA的问题解决了,t4线程修改失败。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值