Java并发编程——CAS

 2023-02-06
原文作者:蒋先森 原文地址:https://jlj98.top/

如果本文有错,希望在下面的留言区指正。

在之前的一些源码分析中,为了实现并发,Doug Lea 大佬在Java8及以上,大量使用了 CAS 操作。JDK 提供的关于 CAS 原子操作的类在下面工具包里面:

202212301147369291.png

JDK为Java基本类型都提供了CAS工具类。

CAS

AtomicInteger 为例,进行分析:

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

如上面的源码,对于 CAS 操作,这里会出现3个值,expect、update、value。只有当expect和内存中的value相同时,才把value更新为update。

ABA 问题

假设如下事件序列:

线程 1 从内存位置V中取出A。
线程 2 从位置V中取出A。
线程 2 进行了一些操作,将B写入位置V。
线程 2 将A再次写入位置V。
线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

尽管上面的CAS操作成功了,数据也没有问题,但是程序失去了对数据变换的敏感性,不知道数据的变换。

比如发生扣款/收款行为时,应当收到短信通知这个场景,
1、时刻1 : 500元
2、时刻2:转给了 A 10 元 490 元
3、时刻3:B 转入 10 元 500 元
应当收到两条短信,而不是最后我的账户余额没有变化,就一条短信都收不到

202212301147387202.png

上面的图片来源不知道是谁的,只是在一个技术群里和别人聊CAS时别人发的。

解决方法

AtomicStampedReference

JDK 为了解决 ABA 问题,提供了一些方法,如 AtomicStampedReference,在版本的更新过程中,添加了一个 stamp 邮戳来标记数据的版本。

    //比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
    public boolean compareAndSet(V expectedReference, V newReference, 
        int expectedStamp, int newStamp)
    //获得当前对象引用
    public V getReference()
    //获得当前时间戳
    public int getStamp()
    //设置当前对象引用和时间戳
    public void set(V newReference, int newStamp)

具体关于 CAS 操作源码

    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);
        }
    }
    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)));
    }

如上面所示,在更新的过程中,除了比较内存中value的预期值,还比较了 stamp 的预期值,只有两者都相同的时候,才会把内存中的值更新掉。

AtomicMarkableReference

AtomicMarkableReferenceAtomicStampedReference 功能相似,但AtomicMarkableReference 描述更加简单的是与否的关系。它的定义就是将状态戳简化为true|false。如下:

    public boolean compareAndSet(V       expectedReference,
                                 V       newReference,
                                 boolean expectedMark,
                                 boolean newMark) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedMark == current.mark &&
            ((newReference == current.reference &&
              newMark == current.mark) ||
             casPair(current, Pair.of(newReference, newMark)));
    }