CAS简介

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

悲观锁

总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。在Java中,synchronized的思想就是悲观锁。

乐观锁

总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。java.util.concurrent包中借助CAS实现了区别于synchronized同步锁的一种乐观锁。

CAS应用

java.util.concurrent包中无论是ReentrantLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了CAS,最常见的就是我们在上一篇遇到的i++这种情况。传统的方法肯定是在方法上加上synchronized关键字:

public volatile int i;
public synchronized void add() {
  i++;
}

这种方法在性能上会比较差,我们可以使用java.util.concurrent包下的AtomicInteger,既能保证i++的原子性,又能提高性能。

public AtomicInteger i;
public void add() {
  i.getAndIncrement();
}

在getAndIncrement()方法中会调用Unsafe的getAndAddInt()

public final int getAndIncrement() {
  return unsafe.getAndAddInt(this, valueOffset, 1);
}

再看看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;
}

这里我们见到compareAndSwapInt这个函数,它就是CAS缩写的由来。

compareAndSwapInt(var1, var2, var5, var5 + var4)其实换成compareAndSwapInt(obj, offset, expect, update)会更好理解,意思就是如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它的值为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作,这也是两个步骤,但是在JNI里是借助于一个CPU指令完成的。所以还是原子操作。

CAS原理

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们继续查看compareAndSwapInt这个方法,在Java中此方法为native方法。

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

底层使用JNI调用C代码实现的,如果你有openJDK源码,那么在Unsafe.cpp里可以找到它的实现。

static JNINativeMethod methods_15[] = {
  ...
  {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
  {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
  ...
};

可以看到compareAndSwapInt实现是在Unsafe_CompareAndSwapInt里面。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

p是取出的对象,addr是p中offset处的地址,最后调用了Atomic::cmpxchg(x, addr, e), 其中参数x是即将更新的值,参数e是原内存的值。

inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

os::is_MP判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。

LOCK_IF_MP根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lockcmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

cmpxchgl会默认比较eax寄存器的值即compare_value和exchange_value的值,如果相等,就把dest的值赋值给exchange_value,否则,将exchange_value赋值给eax。

CAS的问题

1ABA问题

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

 

2循环时间长开销大

如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。