ABA问题的本质及其解决办法
简介
CAS的全称是compare and swap,它是java同步类的基础,java.util.concurrent中的同步类基本上都是使用CAS来实现其原子性的。
CAS的原理其实很简单,为了保证在多线程环境下我们的更新是符合预期的,或者说一个线程在更新某个对象的时候,没有其他的线程对该对象进行修改。在线程更新某个对象(或值)之前,先保存更新前的值,然后在实际更新的时候传入之前保存的值,进行比较,如果一致的话就进行更新,否则失败。
注意,CAS在java中是用native方法来实现的,利用了系统本身提供的原子性操作。
那么CAS在使用中会有什么问题呢?一般来说CAS如果设计的不够完美的话,可能会产生ABA问题,而ABA问题又可以分为两类,我们先看来看一类问题。
更多精彩内容且看:
- 区块链从入门到放弃系列教程-涵盖密码学,超级账本,以太坊,Libra,比特币等持续更新
- Spring Boot 2.X系列教程:七天从无到有掌握Spring Boot-持续更新
- Spring 5.X系列教程:满足你对Spring5的一切想象-持续更新
- java程序员从小工到专家成神之路(2020版)-持续更新中,附详细文章教程
更多内容请访问www.flydean.com
第一类问题
我们考虑下面一种ABA的情况:
- 在多线程的环境中,线程a从共享的地址X中读取到了对象A。
- 在线程a准备对地址X进行更新之前,线程b将地址X中的值修改为了B。
- 接着线程b将地址X中的值又修改回了A。
- 最新线程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。考虑下面的步骤:
- 线程a读取链表头部节点A。
- 线程b将链表中的B节点删掉,链表变成了A->C
- 线程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++之类的语言中解决这个问题。
根据官方的说法,第一类问题大概有四种解法:
- 使用中间节点 - 使用一些不代表任何数据的中间节点来表示某些节点是标记被删除的。
- 使用自动GC。
- 使用hazard pointers - hazard pointers 保存了当前线程正在访问的节点的地址,在这些hazard pointers中的节点不能够被修改和删除。
- 使用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问题其实是由两类问题组成的,需要我们分开来对待和解决。
本文的例子https://github.com/ddean2009/ learn-java-base-9-to-20
本文作者:flydean程序那些事
本文链接:http://www.flydean.com/aba-cas-stamp/
本文来源:flydean的博客
欢迎关注我的公众号:程序那些事,更多精彩等着您!
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
不可忽视的Dubbo线程池
问题描述 线上突然出现Dubbo超时调用,时间刚好为Consumer端设置的超时时间。 有好几个不同的接口都报超时了 第1次调用超时,第2次(或第3次)重试调用非常快(正常水平) Dubbo调用超时的情况集中出现了3次,每次都是过一会自动恢复 排查 排查日志 看到调用超时,首先就拿着traceId去服务提供方查日志。 奇怪的是,在服务提供方的业务日志里面,只有正常的调用日志(耗时正常),没有超时调用的日志。 从正常的调用日志里面看,一切都是正常的,看不出所以然。 给人的感觉就是超时那次请求的调用没有达到服务提供方。 此时系统活动情况 通过系统历史监控,我们发现除了gc比平时稍微高一点外(也在正常水位),没有其他的异常;CPU、内存、网络等指标都在正常范围。 查看Dubbo线程活动情况 第2次系统集中超时报警的做的第一件事就是登录到那台服务器查看dubbo线程活动情况:看下能不能找到阻塞在哪一行代码。很遗憾,所有的dubbo线程都没有阻塞,都是正常的WAITING状态。 并没有明显表明阻塞在某段代码,这可难倒我们了:如果没有阻塞的话,为什么dubbo调用方会报超时?继续看代码 该接口是否...
- 下一篇
序列化/反序列化,我忍你很久了
本文 Github开源项目:github.com/hansonwang99/JavaCollection中已收录,有详细自学编程学习路线、面试题和面经、编程资料及系列技术文章等,资源持续更新中... 工具人 曾几何时,对于Java的序列化的认知一直停留在:「实现个Serializbale接口」不就好了的状态,直到 ... 所以这次抽时间再次重新捧起了尘封已久的《Java编程思想》,就像之前梳理《枚举部分知识》一样,把「序列化和反序列化」这块的知识点又重新审视了一遍。 序列化是干啥用的? 序列化的原本意图是希望对一个Java对象作一下“变换”,变成字节序列,这样一来方便持久化存储到磁盘,避免程序运行结束后对象就从内存里消失,另外变换成字节序列也更便于网络运输和传播,所以概念上很好理解: 序列化:把Java对象转换为字节序列。 反序列化:把字节序列恢复为原先的Java对象。 而且序列化机制从某种意义上来说也弥补了平台化的一些差异,毕竟转换后的字节流可以在其他平台上进行反序列化来恢复对象。 事情就是那么个事情,看起来很简单,不过后面的东西还不少,请往下看。 对象如何序列化? 然而Java目前...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- 设置Eclipse缩进为4个空格,增强代码规范
- Mario游戏-低调大师作品
- MySQL8.0.19开启GTID主从同步CentOS8
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池