温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

CAS是什么?

发布时间:2020-06-11 18:28:14 来源:亿速云 阅读:238 作者:元一 栏目:编程语言

一、什么是CAS

CAS指CompareAndSwap,CAS是支持并发的第一个处理器提供原子的测试并设置操作,通常在单位上运行这项操作。

CAS 操作包含三个操作数 -- 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了"我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。"

二、atomic包

使用java.util.atomic包在java中CAS的实现。

CAS是什么?

这就是java.util.atomic包下的类,我们着重看AtomicInteger源码(其他的都是一样的思想实现的)

CAS有什么缺点?

CAS缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。

2.1、走进AtomicInteger源码
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 使用Unsafe.compareAndSwapInt进行原子更新操作
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //value对应的存储地址偏移量
    private static final long valueOffset;

    static {
        try {
            //使用反射及unsafe.objectFieldOffset拿到value字段的内存地址偏移量,这个值是固定不变的
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //volatile修饰的共享变量
    private volatile int value;
    //..........
    }

上面的代码其实就是为了初始化内存值对应的内存地址偏移量valueOffset,方便后续执行CAS操作时使用。因为这个值一旦初始化,就不会更改,所以使用static final 修饰。

我们可以看到value使用了volatile修饰,其中也说了volatile的语义。

我们都知道如果进行value++操作,并发下是不安全的。上一篇中我们也通过例子证明了volatile只能保证可见性,不能保证原子性。因为value++本身不是原子操作,value++分了三步,先拿到value的值,进行+1,再赋值回value。

2.2、compareAndSwapXxx

我们先看一看AtomicInteger提供的CAS操作。

    /**
     * 原子地将value设置为update,如果valueOffset对应的值与expect相等时
     *
     * @param expect 期待值
     * @param update 更新值
     * @return 如果更新成功,返回true;在valueOffset对应的值与expect不相等时返回false
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

我们已经知道CAS的原理,那来看看下面的测试。你知道输出的结果是多少吗?评论区给出你的答案吧。

public class AtomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.compareAndSet(0, 1);
        atomicInteger.compareAndSet(2, 1);
        atomicInteger.compareAndSet(1, 3);
        atomicInteger.compareAndSet(2, 4);
        System.out.println(atomicInteger.get());
    }
}

Unsafe提供了三个原子更新的方法。

关于Unsafe类,因为java不支持直接操作底层硬件资源,如分配内存等。如果你使用unsafe开辟的内存,是不被JVM垃圾回收管理,需要自己管理,容易造成内存泄漏等。

2.3、AtomicInteger的原子自增方法

我们上面说了,value++不是原子操作,不能在并发下使用。我们来看看AtomicInteger提供的原子++操作。

    /**
     * 原子地对value进行+1操作
     *
     * @return 返回更新后的值
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    /**
     * unsafe提供的方法
     * var1 更改的目标对象
     * var2 目标对象的共享字段对应的内存地址偏移量valueOffset
     * var4 需要在原value上增加的值
     * @return 返回未更新前的值
     */
     public final int getAndAddInt(Object var1, long var2, int var4) {
        //期待值
        int var5;
        do {
            //获取valueOffset对应的value的值,支持volatile load
            var5 = this.getIntVolatile(var1, var2);
            //如果原子更新失败,则一直重试,直到成功。
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

我们看到CAS只能原子的更新一个值,如果我们要原子更新多个值,CAS可以做到吗?答案是可以的。

2.4、AtomicReference

如果要原子地更新多个值,就需要使用AtomicReference。其使用的是compareAndSwapObject方法。可以将多个值封装到一个对象中,原子地更换对象来实现原子更新多个值。

public class MultiValue {
    private int value1;
    private long value2;
    private Integer value3;

    public MultiValue(int value1, long value2, Integer value3) {
        this.value1 = value1;
        this.value2 = value2;
        this.value3 = value3;
    }
 }

public class AtomicReferenceTest {
    public static void main(String[] args) {
        MultiValue multiValue1 = new MultiValue(1, 1, 1);
        MultiValue multiValue2 = new MultiValue(2, 2, 2);
        MultiValue multiValue3 = new MultiValue(3, 3, 3);
        AtomicReference<MultiValue> atomicReference = new AtomicReference<>();
        //因为构造AtomicReference时,没有使用有参构造函数,所以value默认值是null
        atomicReference.compareAndSet(null, multiValue1);
        System.out.println(atomicReference.get());
        atomicReference.compareAndSet(multiValue1, multiValue2);
        System.out.println(atomicReference.get());
        atomicReference.compareAndSet(multiValue2, multiValue3);
        System.out.println(atomicReference.get());
    }
}
//输出结果
//MultiValue{value1=1, value2=1, value3=1}
//MultiValue{value1=2, value2=2, value3=2}
//MultiValue{value1=3, value2=3, value3=3}

我们再看一看AtomicReference的compareAndSet方法。

注意:这里的比较都是使用==而非equals方法。所以最好封装的MultiValue不要提供set方法。

 public final boolean compareAndSet(V expect, V update) {
     return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
 }
2.5、CAS的ABA问题

假设你的账户上有100块钱,你要给女票转50块钱。

我们使用CAS进行原子更新账户余额。由于某种原因,你第一次点击转账出现错误,你以为没有发起转账请求,这时候你又点击了一次。系统开启了两个线程进行转账操作,第一个线程进行CAS比较,发现你的账户上预期是100块钱,实际也有100块钱,这时候转走了50,需要设置为100 - 50 = 50 元,这时账户余额为50

第一个线程操作成功了,第二个线程由于某种原因阻塞住了;这时候,你的家人又给你转了50块钱,并且转账成功。那你账户上现在又是100块钱;

太巧了,第二个线程被唤醒了,发现你的账户是100块钱,跟预期的100是相等的,这时候又CAS为50。大兄弟,哭惨了,你算算,正确的场景你要有多少钱?这就是CAS存在的ABA问题。

public class AtomicIntegerABA {

    private static AtomicInteger atomicInteger = new AtomicInteger(100);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        //线程1
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            atomicInteger.compareAndSet(100, 50);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
        });

        //线程2
        executorService.execute(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            atomicInteger.compareAndSet(50, 100);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
        });

        //线程3
        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
            atomicInteger.compareAndSet(100, 50);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
        });

        executorService.shutdown();
    }
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 50

大家心想,靠,这不是坑吗?那还用。。。。。。。。。。。。。。冷静,冷静。你能想到的问题,jdk都能想到。atomic包提供了一个AtomicStampedReference

2.6、AtomicStampedReference

看名字是不是跟AtomicReference很像啊,其实就是在AtomicReference上加上了一个版本号,每次操作都对版本号进行自增,那每次CAS不仅要比较value,还要比较stamp,当且仅当两者都相等,才能够进行更新。

public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
//定义了内部静态内部类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);
        }
    }

//所以我们之前的value就对应为现在的pair
    private volatile Pair<V> pair;

让我们来看一看它的CAS方法。

 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)));
    }
private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

还是上面转账的例子,我们使用AtomicStampedReference来看看是否解决了呢。

public class AtomicStampedReferenceABA {
    /**
     * 初始化账户中有100块钱,版本号对应0
     */
    private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0);

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        int[] result = new int[1];
        //线程1
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            //将100更新为50,版本号+1
            atomicInteger.compareAndSet(100, 50, 0, 1);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
        });

        //线程2
        executorService.execute(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            //将50更新为100,版本号+1
            atomicInteger.compareAndSet(50, 100, 1, 2);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
        });

        //线程3
        executorService.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
            //此线程还是以为没有其他线程进行过更改,所以旧版本号还是0
            atomicInteger.compareAndSet(100, 50, 0, 1);
            System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
        });

        executorService.shutdown();
    }
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 100
向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI