前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程之CAS算法ABA问题分析和解决

并发编程之CAS算法ABA问题分析和解决

原创
作者头像
小明爱吃火锅
发布2023-11-04 12:10:46
3190
发布2023-11-04 12:10:46
举报
文章被收录于专栏:小明说Java小明说Java

​前言

在前面《并发编程之CAS算法与原子变量详解》我们采用JUC包下的Atomic原子变量,解决了多线程环境下共享变量原子性问题,Atomic底层操作是基于CAS算法,并且也提到,采用一种无锁的非阻塞算法的实现,乐观锁算法,但是也会有一些缺点。

其中有一个就是ABA问题,CAS原理其实就是拿副本中的预期值与主存中的值作比较,如果相等就继续替换新值,如果不相等就说明主存中的值已经被别的线程修改,就继续重试,这期间如果并发请求过多,有其中一步慢,就有可能出现问题,本文就来重点分析CASABA问题并怎么解决。

一、什么是ABA问题

ABA问题是指,当一个线程T1从内存地址X中取出值A,另一个线程T2也从内存地址X中取出值A,然后T2进行了一系列操作将值改变成B,写回主物理内存。接着,T2又将内存地址为X的数据变为A,这时线程T1进行了CAS操作发现内存中仍然是A,于是T1操作成功。尽管线程T1的CAS操作成功,但并不代表这个过程就没有问题,实际地址已经改变了。

ABA问题的产生原因是,CAS操作只检查内存中的数据值是否与预期值相同,而不会检查该值在内存中的变化过程。因此,当数据值在内存中发生变化时,CAS操作可能会误判为该值未被其他线程修改过。

简单描述:

假如说你有一个值,我拿到这个值是0,想把它变成2,我拿到1用cas操作,期望值是1,准备变成2,对象是Object,在这个过程中没有一个线程改过我这个值,肯定可修改。如果有一个线程在这个过程中把这个1修改成了2后来又变回1,中间值更改过但是不影响我后面的操作,这就是ABA问题。

二、怎么解决ABA问题

如果是int类型的,最终值是你期望的,没有关系。确实想要解决的话,就是加版本,做任何一个值的修改,修改完加一,后面检查的时候连同版本号一起检查。但是对于共享对象,如果有线程改变了对象的值,但是对象的地址其实没有变,这样其他线程在读取拿到的对象跟期望会认为是一样的,依然修改成功。所以针对对象,必须严格控制。

1、ABA问题

我们在讲解原子变量的时候,使用了AtomicInteger保证多线程共享变量原子性,但是一个线程A由于等了一会,这个间隙第一个线程B已经操作了这个数据,但是另一个线程A不清楚的,以为值还是之前的,导致修改成功。如果判断加一之后,才可以调用接口,线程A有可能在这个期间调用了业务接口,然后在吧数据改回去。

代码语言:javascript
复制
public class ABADemo {

    // AtomicInteger也是会出现ABA
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0);

    public static void main(String[] args) throws InterruptedException {
        // 其中一个线程改了,又改回去
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.compareAndSet(100, 101);
                // 这期间处理业务,比如说调用业务接口
                if(atomicInteger.get().equals(101)){
                    System.out.println("todoAAA");
                }
                atomicInteger.compareAndSet(101, 100);
            }
        });

        // 另一个线程由于等了一会,这个间隙intT1已经操作了这个数据,但是intT2不清楚的,以为值还是之前的,导致修改成功
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean flag = atomicInteger.compareAndSet(100, 101);// true
                System.out.println("thread intT2: "  + atomicInteger);
                System.out.println("flag is " + flag);
                 if(atomicInteger.get().equals(101)){
                    System.out.println("todoAAA");
                }
            }
        });

        intT1.start();
        intT2.start();

}
}

运行结果如下,可以看到,第一个线程修改的修改了之后,调用业务比如todoAAA,然后在改回去,第二线程以为没有修改,同样可以修改数据,再次调用业务接口todoAAA,最终业务接口会被调用两次。

2、原子对象解决ABA问题

我们已经看到AtomicInteger虽然保证多线程共享变量原子性,但是会出现ABA问题,当然,要解决这个问题JUC也是提供了原子对象AtomicStampedReference来解决这个问题。

其实原理也是增加了版本号控制,每次操作之后,修改的值是否改变,版本号都+1,所以上面ABA问题可以改成如下代码:

代码语言:javascript
复制
public class ABADemo {

    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100,0);

    public static void main(String[] args) throws InterruptedException {


        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100,101,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() + 1);
                atomicStampedRef.compareAndSet(101,100,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() + 1);
            }
        });

        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());
                boolean flag = atomicStampedRef.compareAndSet(100,101,stamp,stamp + 1);
                System.out.println("thread refT2: " + atomicStampedRef.getReference() + ", flag is " + flag);
            }
        });
        refT1.start();
        refT2.start();
    }
}

可以看到运行结果,线程2睡眠唤醒之后,发现版本号stamp变成2了 ,这个导致前后读取期望的版本号不一致了,线程2就不能在修改。这样的话,线程1及时偷偷调用了业务接口,线程2也会发现,并且不会在调用。

总结

总之,解决ABA问题是并发编程中的一个重要问题。解决ABA问题的归根到底还是使用版本号。在每个变量值修改时,增加一个版本号,并将版本号一起进行CAS操作。如果CAS操作失败,可以通过比较版本号来判断是否存在ABA问题。如果版本号相同,说明存在ABA问题;如果版本号不同,说明变量值已经被其他线程修改了

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ​前言
  • 一、什么是ABA问题
  • 二、怎么解决ABA问题
    • 1、ABA问题
      • 2、原子对象解决ABA问题
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档