要了解ABA问题,我们得先知道什么是CAS,CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS的出现主要是为了解决多线程并发情况下,数据的不一致问题。
CAS底层原理
CAS 的思想很简单:三个参数,一个当前内存值 V、旧的预期值 A、即将更新的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false
Unsafe类
Unsafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,基于该类可以直接操作特定内存的数据。Unsafe类存在与sum.misc包中,其内部实现是C++写的,我从JDK1.8源码中截取了关键代码
- UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
- UnsafeWrapper("Unsafe_CompareAndSwapInt");
- oop p = JNIHandles::resolve(obj);
- jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
- return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
- UNSAFE_END
从上面代码可以看出最后调用的是Atomic:comxchg这个方法,这个方法的实现放在hotspot下的os_cpu包中,说明这个方法的实现和操作系统、CPU都有关系,以多核CPU为例:
CAS问题
cas实现
从JDK1.5开始,java.util.concurrent包为我们提供了许多cas操作类诸如:AtomicInteger,
AtomicLong,AtomicReference
上图运行过程中可能会出现两个问题:
ABA问题的优化
ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,再有些情况下,“值”相同不会引入错误的业务逻辑(例如库存),有些情况下,“值”虽然相同,却已经不是原来的数据了。那如何能避免ABA问题呢?优化的方式也很简单,就是不能只对值进行比较,通过对值打标签的方式就能很好的避免ABA问题。JAVA中也为我们提供了相应的处理类AtomicStampReferenceAtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。它有以下几个参数:
- //参数代表的含义分别是 期望值,写入的新值,期望标记,新标记值
- public boolean compareAndSet(V expected,V newReference,int expectedStamp,int newStamp);
- public V getRerference();
- public int getStamp();
- public void set(V newReference,int newStamp);
我们通过一个示例来说明:
- public class Test {
- private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
- public static void main(String[] args) {
- new Thread(() -> {
- atomicReference.compareAndSet(100, 101);
- atomicReference.compareAndSet(101, 100);
- },"t1").start();
- new Thread(() -> {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(atomicReference.compareAndSet(100, 2021) + "\t修改后的值:" + atomicReference.get());
- },"t2").start();
- }
- }
可以看到,线程2修改成功。输出结果:
- true 修改后的值:2021
要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号, 当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功
- public class Test {
- private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1);
- public static void main(String[] args) {
- new Thread(() -> {
- System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp());
- //睡眠1秒,是为了让t2线程也拿到同样的初始版本号
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
- atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
- },"t1").start();
- new Thread(() -> {
- int stamp = atomicStampedReference.getStamp();
- System.out.println("t2拿到的初始版本号:" + stamp);
- //睡眠3秒,是为了让t1线程完成ABA操作
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("最新版本号:" + atomicStampedReference.getStamp());
- System.out.println(atomicStampedReference.compareAndSet(100, 2021,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference());
- },"t2").start();
- }
- }
输出结果:
- t1拿到的初始版本号:1
- t2拿到的初始版本号:1
- 最新版本号:3
- false当前 值:100
端点路由(Endpoint Routing)最早出现在ASP.NET Core2.2,在ASP.NET Core3.0提升...
详解JSP中使用过滤器进行内容编码的解决办法 问题 当通过JSP页面,向数据库中插...
目录中出现 jsconfig.json 文件表明该目录是 JavaScript 项目的根目录。 Json 文...
MySQL的binlog相信大家都有所耳闻,但是可能没有真正日常使用过。 因此,本文结...
本文转载自微信公众号「三太子敖丙」,作者三太子敖丙。转载本文请联系三太子敖...
前言 静态文件(如 HTML、CSS、图像和 JavaScript)等是Web程序的重要组成部分。...
通过ImageMagickObject的identify获取图片的信息,在命令行下好用,但是放到程序...
为什么我们需要它 不得不说,在知道这个命令的时,以及之后的使用中,我都超级热...
一、GIF图 二、前台代码 // 调用方法 hotlineLine(); // 定时刷新 setInterval(f...
博主最近在做一个个人的博客网站,准备用 thymeleaf 实现一个动态加载一二级文章...