侧边栏壁纸
博主头像
小明锅博主等级

没错,我就是小明,不过已经长大了,成为一名码农,在搬砖的同时,喜欢分享Java的编程知识,本网站致力于一站式后端人员开发,解决码农日常问题,挤出更多moyu时间

  • 累计撰写 16 篇文章
  • 累计创建 10 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录

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

小明锅
2024-08-20 / 0 评论 / 0 点赞 / 295 阅读 / 3,569 字 / 正在检测是否收录...

前言

在前面《并发编程之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有可能在这个期间调用了业务接口,然后在吧数据改回去。

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问题可以改成如下代码:

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问题;如果版本号不同,说明变量值已经被其他线程修改了

0
广告 广告

评论区