首页 > 其他 > 详细

CAS解析

时间:2021-02-18 15:06:05      阅读:25      评论:0      收藏:0      [点我收藏+]

CAS全称是 compare and swap, 即比较并交换, 它是一种原子操作, 同时 CAS 是一种乐观机制. java.util.concurrent 包很多功能都是建立在 CAS 之上, 如 ReenterLock 内部的 AQS, 各种原子类, 其底层都用 CAS来实现原子操作.

1. sun.misc.Unsafe

简单讲一下这个类. Java 无法直接访问底层操作系统, 而是通过本地 (native) 方法来访问. 不过尽管如此, JVM 还是开了一个后门, JDK 中有一个类 Unsafe, 它提供了硬件级别的原子操作. 这个类尽管里面的方法都是 public 的, 但是并没有办法使用它们, JDK API 文档也没有提供任何关于这个类的方法的解释. 总而言之, 对于 Unsafe 类的使用都是受限制的, 只有授信的代码才能获得该类的实例, 当然 JDK 库里面的类是可以随意使用的.

从第一行的描述可以了解到 Unsafe 提供了硬件级别的操作, 比如说获取某个属性在内存中的位置, 比如说修改对象的字段值, 即使它是私有的. 不过Java本身就是为了屏蔽底层的差异, 对于一般的开发而言也很少会有这样的需求.

简单看下该类中的几个方法:

// 可以用来获取给定的 paramField 的内存地址偏移量, 这个值对于给定的 field 是唯一的且是固定不变的. 
public native long staticFieldOffset(Field paramField); 
public native long allocateMemory(long paramLong); // 分配内存
public native long reallocateMemory(long paramLong1, long paramLong2); // 扩充内存
public native void freeMemory(long paramLong); // 释放内存的

2. CAS

CAS (Compare and Swap) 即比较并交换, 设计并发算法时常用到的一种技术, java.util.concurrent 包全完建立在 CAS 之上, 没有 CAS 也就没有此包, 可见 CAS 的重要性.

***CAS 有三个操作数: 内存值 V, 旧的预期值 A, 要修改的对象 B, 当且仅当预期值 A 和内存值 V 相同时, 将内存值修改为 B 并返回 true, 否则什么都不做并返回 false. ***

CAS 也是通过 Unsafe 实现的, 看下 Unsafe 下的三个方法:

// 包含要修改的字段对象, 字段在对象内的偏移量, 字段的期望值, 用于更新字段的新值
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

不使用 CAS 的情况下, 如果要实现一个比较并交换 int 值的方法, 那么代码大致是这样的:

public int value = 1;
public boolean compareAndSwapInt(int param) {
    if(value == param) {
        value = param;
        return true;
    }
    return false;
}

以上代码如果在并发下运行肯定是有问题的, 解决办法也很简单, 给该方法加锁同步变成一个原子操作就可以了. CAS 也是一样的道理, 比较, 交换也是一组原子操作, 不会被外部打断, 先根据 paramLong/paramLong1 获取到内存当中当前的内存值 V, 在将内存值 V 和原值 A 作比较, 要是相等就修改为要修改的值 B, 由于 CAS 都是硬件级别的操作, 因此效率会高一些.

3. 借 AtomicInteger 分析 CAS

public class AtomicInteger extends Number implements java.io.Serializable {

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { 
            throw new Error(ex); 
        }
    }

    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    public final int get() {
        return value;
    }
    ......
}

以上代码是 JDK1.8 中 AtomicInteger 的部分代码, 关于代码中出现的几个成员变量:

  1. unsafe 是 CAS 的核心, 查看前面部分说明.
  2. valueOffset 是变量值在内存中的偏移地址, 因为 Unsafe 就是根据内存偏移地址获取数据的原值的.
  3. value 是用 volatile 修饰的, 这是非常关键的.

再来查看下 addAndGet 方法, 该方法底层利用 CAS 技术保证了并发安全, 其中 unsafe.getAndAddInt() 方法如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 自旋
    return var5;
}

var5通过this.getIntVolatile(var1, var2)方法获取, 是个 native 方法, 其目的是获取 var1 在 var2 偏移量的值, 其中 var1 就是 AtomicInteger, var2 是 valueOffset 值.

上述代码中 compareAndSwapInt 就是实现 CAS 的核心方法,其原理是如果 var1 中的 value 值和 var5 相等, 就证明没有其他线程改变过这个变量, 那么就把 value 值更新为 var5 + var4, 其中 var4 是更新的增量值;反之如果没有更新, 那么 CAS 就一直采用自旋的方式继续进行操作(其实就是个 while 死循环), 这一步也是一个原子操作.

以上代码的执行过程:

  1. 设定 AtomicInteger 的 value 原始值为 A, 从 Java 内存模型得知, 线程1和线程2各自持有一份 value 的副本, 值都是 A.
  2. 线程1通过 getIntVolatile(var1, var2) 拿到 value 值 A, 这时线程1被挂起.
  3. 线程2也通过 getIntVolatile(var1, var2) 方法获取到 value 值 A, 并执行 compareAndSwapInt 方法比较内存值也为 A, 成功修改内存值为 B.
  4. 这时线程1恢复执行 compareAndSwapInt 方法比较, 发现自己手里的值 A 和内存的值 B 不一致, 说明该值已经被其它线程提前修改过了.
  5. 线程1重新执行 getIntVolatile(var1, var2) 再次获取 value 值, 因为变量 value 被 volatile 修饰, 所以其它线程对它的修改, 线程1总是能够看到, 线程1继续执行 compareAndSwapInt 进行比较替换, 直到成功.

4. CAS常见问题

4.1 ABA 问题

例如线程1从内存位置 V 取出 A, 这时候线程2也从内存位置 V 取出 A, 此时线程1处于挂起状态, 线程2将位置 V 的值改成 B, 最后再改成 A, 这时候线程1再执行, 发现位置 V 的值没有变化, 尽管线程1也更改成功了, 但是不代表这个过程就是没有问题的. java.util.concurrent 包为了解决这个问题, 提供了一个带有标记的原子引用类 AtomicStampedReference, 它可以通过控制变量值的版本来保证 CAS 的正确性.

现有一个用单向链表实现的栈, 栈顶元素为 A, A.next 为 B, 期望用 CAS 将栈顶替换成 B.

现有线程1获取了元素 A, 此时线程1被挂起, 线程2也获取了元素 A, 并将 A, B 出栈, 再push D, C, A, 这时线程1恢复执行 CAS, 因为此时栈顶元素依然为 A, 线程1执行成功, 栈顶元素变成了 B, 但 B.next 为 null, 这就会导致 C, D 被丢掉了.

4.2 自旋问题

从源码可以知道所说的自旋无非就是操作结果失败后继续循环操作, 这种操作也称之为自旋锁, 是一种乐观锁机制, 一般来说都会给一个限定的自选次数, 防止进入死循环.

自旋锁的优点是不需要休眠当前线程, 因为自旋锁使用者一般保持锁时间非常短, 因此选择自旋而不是休眠当前线程是提高并发性能的关键点, 这是因为减少了很多不必要的线程上下文切换开销. 但是, 如果CAS一直操作不成功, 会造成长时间原地自旋, 会给CPU带来非常大的执行开销.

CAS解析

原文:https://www.cnblogs.com/annwyn/p/14411654.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!