分析ABA问题的本质及其解决办法

 更新时间:2021年06月02日 10:58:22   作者:flydean  
CAS的全称是compare and swap,它是java同步类的基础,java.util.concurrent中的同步类基本上都是使用CAS来实现其原子性的。本文将介绍ABA问题的本质及其解决办法。

简介

CAS的原理其实很简单,为了保证在多线程环境下我们的更新是符合预期的,或者说一个线程在更新某个对象的时候,没有其他的线程对该对象进行修改。在线程更新某个对象(或值)之前,先保存更新前的值,然后在实际更新的时候传入之前保存的值,进行比较,如果一致的话就进行更新,否则失败。

注意,CAS在java中是用native方法来实现的,利用了系统本身提供的原子性操作。

那么CAS在使用中会有什么问题呢?一般来说CAS如果设计的不够完美的话,可能会产生ABA问题,而ABA问题又可以分为两类,我们先看来看一类问题。

第一类问题

我们考虑下面一种ABA的情况:

1.在多线程的环境中,线程a从共享的地址X中读取到了对象A。

2.在线程a准备对地址X进行更新之前,线程b将地址X中的值修改为了B。

3.接着线程b将地址X中的值又修改回了A。

4.最新线程a对地址X执行CAS,发现X中存储的还是对象A,对象匹配,CAS成功。

上面的例子中CAS成功了,但是实际上这个CAS并不是原子操作,如果我们想要依赖CAS来实现原子操作的话可能就会出现隐藏的bug。

第一类问题的关键就在2和3两步。这两步我们可以看到线程b直接替换了内存地址X中的内容。

在拥有自动GC环境的编程语言,比如说java中,2,3的情况是不可能出现的,因为在java中,只要两个对象的地址一致,就表示这两个对象是相等的。

2,3两步可能出现的情况就在像C++这种,不存在自动GC环境的编程语言中。因为可以自己控制对象的生命周期,如果我们从一个list中删除掉了一个对象,然后又重新分配了一个对象,并将其add back到list中去,那么根据 MRU memory allocation算法,这个新的对象很有可能和之前删除对象的内存地址是一样的。这样就会导致ABA的问题。

第二类问题

如果我们在拥有自动GC的编程语言中,那么是否仍然存在CAS问题呢?

考虑下面的情况,有一个链表里面的数据是A->B->C,我们希望执行一个CAS操作,将A替换成D,生成链表D->B->C。考虑下面的步骤:

1.线程a读取链表头部节点A。

2.线程b将链表中的B节点删掉,链表变成了A->C

3.线程a执行CAS操作,将A替换从D。

最后我们的到的链表是D->C,而不是D->B->C。

问题出在哪呢?CAS比较的节点A和最新的头部节点是不是同一个节点,它并没有关心节点A在步骤1和3之间是否内容发生变化。

我们举个例子:

public void useABAReference(){
    CustUser a= new CustUser();
    CustUser b= new CustUser();
    CustUser c= new CustUser();
    AtomicReference<CustUser> atomicReference= new AtomicReference<>(a);
    log.info("{}",atomicReference.compareAndSet(a,b));
    log.info("{}",atomicReference.compareAndSet(b,a));
    a.setName("change for new name");
    log.info("{}",atomicReference.compareAndSet(a,c));
}

上面的例子中,我们使用了AtomicReference的CAS方法来判断对象是否发生变化。在CAS b和a之后,我们将a的name进行了修改,我们看下最后的输出结果:

[main] INFO com.flydean.aba.ABAUsage - true

[main] INFO com.flydean.aba.ABAUsage - true

[main] INFO com.flydean.aba.ABAUsage - true

三个CAS的结果都是true。说明CAS确实比较的两者是否为统一对象,对其中内容的变化并不关心。

第二类问题可能会导致某些集合类的操作并不是原子性的,因为你并不能保证在CAS的过程中,有没有其他的节点发送变化。

第一类问题的解决

第一类问题在存在自动GC的编程语言中是不存在的,我们主要看下怎么在C++之类的语言中解决这个问题。

根据官方的说法,第一类问题大概有四种解法:

1.使用中间节点 - 使用一些不代表任何数据的中间节点来表示某些节点是标记被删除的。

2.使用自动GC。

3.使用hazard pointers - hazard pointers 保存了当前线程正在访问的节点的地址,在这些hazard pointers中的节点不能够被修改和删除。

4.使用read-copy update (RCU) - 在每次更新的之前,都做一份拷贝,每次更新的是拷贝出来的新结构。

第二类问题的解决

第二类问题其实算是整体集合对象的CAS问题了。一个简单的解决办法就是每次做CAS更新的时候再添加一个版本号。如果版本号不是预期的版本,就说明有其他的线程更新了集合中的某些节点,这次CAS是失败的。

我们举个AtomicStampedReference的例子:

public void useABAStampReference(){
    Object a= new Object();
    Object b= new Object();
    Object c= new Object();
    AtomicStampedReference<Object> atomicStampedReference= new AtomicStampedReference(a,0);
    log.info("{}",atomicStampedReference.compareAndSet(a,b,0,1));
    log.info("{}",atomicStampedReference.compareAndSet(b,a,1,2));
    log.info("{}",atomicStampedReference.compareAndSet(a,c,0,1));
}

AtomicStampedReference的compareAndSet方法,多出了两个参数,分别是expectedStamp和newStamp,两个参数都是int型的,需要我们手动传入。

总结

ABA问题其实是由两类问题组成的,需要我们分开来对待和解决。

以上就是分析ABA问题的本质及其解决办法的详细内容,更多关于ABA问题的本质及其解决办法的资料请关注脚本之家其它相关文章!

相关文章

  • Java开发SSM框架微信支付的实现

    Java开发SSM框架微信支付的实现

    这篇文章主要介绍了Java开发SSM框架微信支付的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-10-10
  • Java编程中的4种代码块详解

    Java编程中的4种代码块详解

    在本篇内容里小编个总结了Java编程中的4种代码块相关的知识点,有兴趣的朋友们可以学习下。
    2021-06-06
  • SpringBoot使用布隆过滤器解决缓存穿透问题

    SpringBoot使用布隆过滤器解决缓存穿透问题

    缓存穿透是指当缓存系统中无法命中需要的数据时,会直接请求底层存储系统(如数据库),但是如果请求的数据根本不存在,那么大量的请求就会直接穿透缓存层,本文将给大家介绍一下SpringBoot使用布隆过滤器解决缓存穿透问题,需要的朋友可以参考下
    2023-10-10
  • Java实现简单推箱子游戏

    Java实现简单推箱子游戏

    这篇文章主要为大家详细介绍了Java实现推箱子游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-06-06
  • 完美解决java.lang.OutOfMemoryError处理错误的问题

    完美解决java.lang.OutOfMemoryError处理错误的问题

    下面小编就为大家带来一篇完美解决java.lang.OutOfMemoryError处理错误的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • Spring Cloud Gateway自定义异常处理Exception Handler的方法小结

    Spring Cloud Gateway自定义异常处理Exception Handler的方法小结

    这篇文章主要介绍了Spring Cloud Gateway自定义异常处理Exception Handler的方法,本文通过两种方法结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-08-08
  • IntelliJ IDEA 的使用界面图文教程

    IntelliJ IDEA 的使用界面图文教程

    这篇文章主要介绍了IntelliJ IDEA 的使用界面图文教程,需要的朋友可以参考下
    2018-10-10
  • 深入理解Java设计模式之代理模式

    深入理解Java设计模式之代理模式

    这篇文章主要介绍了Java设计模式之代理模式的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下
    2021-11-11
  • 深入SQLite多线程的使用总结详解

    深入SQLite多线程的使用总结详解

    本篇文章是对SQLite多线程的使用进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • SpringBoot中如何处理不同的类型的POST请求

    SpringBoot中如何处理不同的类型的POST请求

    在Web开发中,POST请求是非常常见的,用于向服务器提交数据,根据数据的编码方式,POST请求可以分为form-data、x-www-form-urlencoded和raw三种类型,本文将介绍这三种请求方式的区别,并展示如何在Spring Boot中编写代码来处理它们,需要的朋友可以参考下
    2024-08-08

最新评论