解决CAS机制中ABA问题的AtomicStampedReference详解

简介: AtomicStampedReference是一个带有时间戳的对象引用,能很好的解决CAS机制中的ABA问题,这篇文章将通过案例对其介绍分析。

一、ABA问题


ABA问题是CAS机制中出现的一个问题,他的描述是这样的。我们直接画一张图来演示,

v2-610052060a19448523db6007e4610968_1440w.jpg

什么意思呢?就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A。这就是有名的ABA问题。ABA问题会带来什么后果呢?我们举个例子。


一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆嘛?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。


如何去解决这个ABA问题呢,就是使用今天所说的AtomicStampedReference。


二、AtomicStampedReference


1、问题解决


我们先给出一个ABA的例子,对ABA问题进行场景重现。

public class AtomicTest {
    private static AtomicInteger index = new AtomicInteger(10);
    public static void main(String[] args) {
        new Thread(() -> {
            index.compareAndSet(10, 11);
            index.compareAndSet(11, 10);
            System.out.println(Thread.currentThread().getName()+
                    ": 10->11->10");
        },"张三").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
                boolean isSuccess = index.compareAndSet(10, 12);
                System.out.println(Thread.currentThread().getName()+
                        ": index是预期的10嘛,"+isSuccess
                        +"   设置的新值是:"+index.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"李四").start();
    }
}

在上面的代码中,我们使用张三线程,对index10->11->10的变化,然后李四线程读取index观察是否有变化,并设置新值。运行一下看看结果:

v2-b71e75b3d60d9e635fade3b9d686d828_1440w.jpg

这个案例重现了ABA的问题场景,下面我们看如何使用AtomicStampedReference解决这个问题的。

public class AtomicTest2 {
    private static AtomicInteger index = new AtomicInteger(10);
    static AtomicStampedReference<Integer> stampRef 
                            = new AtomicStampedReference(10, 1);
    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampRef.getStamp();
            System.out.println(Thread.currentThread().getName() 
                    + " 第1次版本号: " + stamp);
            stampRef.compareAndSet(10, 11,stampRef.getStamp(),stampRef.getStamp()+1);
            System.out.println(Thread.currentThread().getName() 
                    + " 第2次版本号: " + stampRef.getStamp());
            stampRef.compareAndSet(11, 10,stampRef.getStamp(),stampRef.getStamp()+1);
            System.out.println(Thread.currentThread().getName() 
                    + " 第3次版本号: " + stampRef.getStamp());
        },"张三").start();
        new Thread(() -> {
            try {
                int stamp = stampRef.getStamp();
                System.out.println(Thread.currentThread().getName() 
                        + " 第1次版本号: " + stamp);
                TimeUnit.SECONDS.sleep(2);
                boolean isSuccess =stampRef.compareAndSet(10, 12,
                        stampRef.getStamp(),stampRef.getStamp()+1);
                System.out.println(Thread.currentThread().getName() 
                        + " 修改是否成功: "+ isSuccess+" 当前版本 :" + stampRef.getStamp());
                System.out.println(Thread.currentThread().getName() 
                        + " 当前实际值: " + stampRef.getReference());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"李四").start();
    }
}

上面的代码我们再来分析一下,我们会发现AtomicStampedReference里面增加了一个时间戳,也就是说每一次修改只需要设置不同的版本好即可。我们先运行一边看看:

v2-e95e6bfb995285a0bf301c1bd9c374ab_1440w.jpg

这里使用的是AtomicStampedReference的compareAndSet函数,这里面有四个参数:

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。


(1)第一个参数expectedReference:表示预期值。

(2)第二个参数newReference:表示要更新的值。

(3)第三个参数expectedStamp:表示预期的时间戳。

(4)第四个参数newStamp:表示要更新的时间戳。


这个compareAndSet方法到底是如何实现的,我们深入到源码中看看。


2、源码分析


public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

刚刚这四个参数的意思已经说了,我们主要关注的就是实现,首先我们看到的就是这个Pair,因此想要弄清楚,我们再看看这个Pair是什么,

private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

在这里我们会发现Pair里面只是包存了值reference和时间戳stamp。

在compareAndSet方法中最后还调用了casPair方法,从名字就可以看到,主要是使用CAS机制更新新的值reference和时间戳stamp。我们可以进入这个方法中看看。

//底层调用的是UNSAFE的compareAndSwapObject方法
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }


三、总结


其实除了AtomicStampedReference类,还有一个原子类也可以解决,就是AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用。

相关文章
|
7月前
|
算法 Java
浅谈一下CAS和ABA问题
浅谈一下CAS和ABA问题
75 0
|
存储 资源调度 安全
H3C CAS系列 一、CAS初认识
对于虚拟化,可能第一时间大家想到的是虚拟机,而对于虚拟机大家可能第一时间想到的就是我们大多数人都可能比较熟悉的VMware系列产品,比如常用VMware Workstation Pro 、VMware esxi。 而今天我带大家一起认识一款我们国产的虚拟化软件 H3C CAS。
1788 0
|
2月前
|
安全
【多线程】CAS、ABA问题详解
【多线程】CAS、ABA问题详解
27 0
|
4月前
|
Java
JUC(10)深入理解CAS和ABA
这篇文章深入探讨了Java中的CAS(Compare-And-Swap,比较并交换)操作,解释了CAS的工作原理、应用场景以及存在的循环耗时、单变量原子性保证和ABA问题等缺点,并介绍了如何使用原子引用和版本号来解决ABA问题。
JUC(10)深入理解CAS和ABA
|
7月前
|
Java API
CAS的超~详细介绍
CAS的超~详细介绍
|
7月前
详解CAS及ABA问题
详解CAS及ABA问题
112 0
什么是 CAS? CAS 有哪些缺点?ABA 问题是什么?
什么是 CAS? CAS 有哪些缺点?ABA 问题是什么?
212 0
|
Java Linux 调度
多线程【锁策略与CAS的ABA问题】
多线程【锁策略与CAS的ABA问题】
多线程【锁策略与CAS的ABA问题】
|
算法 Java 数据库
深入理解CAS
深入理解CAS
208 0
深入理解CAS