并发基础 CAS

CAS(Compare and Swap),译为比较并交换。 java.util.concurrent 包实现的基础正是 CAS,是乐观锁思想的运用。

CAS 的基本思想:先进行操作,如果没有其他线程争用共享数据,操作成功;如果共享数据有争用,产生冲突,不断地重试,直到成功为止。

这种乐观并发的策略需要硬件指令集的支持。这类指令常用的有:

  • 测试并设置(Test and Set)
  • 获取并增加(Fetch and Increment)
  • 交换(Swap)
  • 比较并交换(CAS)

其中,CAS 指令是现代处理器新增的,在 IA64、x86 指令集中,可以通过 cmpxchg 指令完成 CAS 功能。

CAS 指令需要有3个操作数,分别是内存位置(V)、预期原值(A)和新值(B)。CAS 指令执行时,如果内存位置的值与预期原值相匹配,将该位置值更新为新值 ;否则,重新尝试,直到成功为止。


Java中的CAS

java.util.concurrent 包是 jdk1.5 中新增的并发工具包,其中使用了大量的 CAS 操作。该操作内部借助 sun.misc.Unsafe 类里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法完成,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果为一条平台相关的 CAS 指令。

从整体来看,concurrent 包的实现示意图如下:

以 java.util.concurrent.atomic 中的原子操作类 AtomicInteger 中的方法 incrementAndGet() 方法为例,看一下 CAS 是如何实现的(JDK1.8版本)。

/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

incrementAndGet() 方法相当于 i++ 的操作,但是以原子的方式执行。可以看到,其内部是调用了 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;
}

public native int getIntVolatile(Object var1, long var2);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

其中 getIntVolatile() 与 compareAndSwapInt() 方法都属于native方法,借助 JNI 完成 CAS 的底层调用。

举例来说,假设现在 AtomicInteger 中的value值为3,线程A和线程B同时执行 getAndAdd 操作(根据 Java 内存模型,线程A和线程B各自持有一份value的副本):

  • 线程A通过 getIntVolatile(var1, var2) 方法获取到value值3,线程切换,线程A挂起
  • 线程B通过 getIntVolatile(var1, var2) 方法获取到value值3,并利用 compareAndSwapInt 方法比较内存值也为3,比较成功,修改内存值为2,线程切换,线程B挂起
  • 线程A恢复,通过 compareAndSwapInt() 方法比较,发现持有的值和内存值不一致,此时线程A无法修改value值,因此重新获取 value 值。因为 value 是 volatile 变量,所以线程对它的修改,线程A总是能够看到。
  • 线程A继续通过 compareAndSwapInt() 进行比较并替换,直到 compareAndSwapInt() 方法返回true

注意:jdk1.7 及之前的 Unsafe 中,没有 getAndAddInt() 方法,采用的是如下实现方式,受影响的还有 getAndAdd、addAndGet 等大部分方法:

public final int getAndIncrement() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}

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

CAS存在的问题

虽然 CAS 相比使用悲观锁,避免了独占对象的现象,所需的开销更小,提高了并发性能,但存在以下问题:

  • ABA问题。如果一个值原来是A,变成了B,又变成了A,那么使用 CAS 进行检查时会认为它的值没有发生变化。为了解决这个问题,atomic 包里提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过控制变量值的版本来保证 CAS 的正确性。不过大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效
  • 长时间自旋开销大。如果 CAS 长时间不成功一直自旋,会给 CPU 带来非常大的执行开销
  • 只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS无法保证操作的原子性,这个时候可以用锁,或者把多个共享变量放到一个对象里来操作
参考链接:

猜你喜欢

转载自blog.csdn.net/weixin_43320847/article/details/83240250