什么是 CAS
CAS,即 compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试
Java 中的 CAS
JDK 1.5 中新增的 java.util.concurrent (JUC) 就是建立在 CAS 之上的,
无论是 ReenterLock 内部的 AQS,还是各种 Atomic 开头的原子类,内部都应用到了 CAS
我们都知道 i++ 在并发编程时存在线程安全问题,这里以 java.util.concurrent.atomic.AtomicInteger 为例,看看 AtomicInteger 在处理 i++ 逻辑时是如何保证线程安全的
public class AtomicIntegerTest {
private volatile int i = 0;
private AtomicInteger atomicInteger = new AtomicInteger();
@Test
void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1000);
for (int j = 0; j < 1000; j++) {
new Thread(() -> {
atomicInteger.getAndIncrement();
i++;
latch.countDown();
}).start();
}
while (latch.getCount() != 0) {
Thread.sleep(10);
}
System.out.format("atomicInteger=%d%n", atomicInteger.get());
System.out.format("i=%d%n", i);
}
}
多次执行会发现 i 有可能小于 1000,而 atomicInteger 永远等于 1000,来看一下 AtomicInteger 的 getAndIncrement 方法源码
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
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); }
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
我们发现,getAndIncrement 方法实际上是调用了 unsafe 的 getAndAddInt 方法,接着看 getAndAddInt 源码
public final class Unsafe {
/**
* 在这里各参数的含义
* @param var1 atomicInteger
* @param var2 valueOffset,偏移量
* @param var4 increment number
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
// var5 expect number
int var5;
do {
// getIntVolatile 从内存中获取 var1 在 var2 偏移量处的值
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}
在这里我们发现 compareAndSwapInt 方法,这个方法实际也是 Java 实现 CAS 的核心方法,从方法名上也能有所发现。compareAndSwapInt(var1, var2, var5, var5 + var4) 其实换成 compareAndSwapInt(obj, offset, expect, update) 更容易理解,意思就是如果 obj 在 offset 处的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作(这里很巧妙的使用了 do...while 来实现自旋操作)
CAS 底层原理
CAS 底层使用 JNI 调用 C 代码实现的,如果你有 Hotspot 源码,那么在 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_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 是原内存的值。代码中能看到 cmpxchg 有基于各个平台的实现,这里选择 Linux X86 平台下的源码分析:
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;
}
这是一段小汇编,__asm__ 说明是 ASM 汇编,volatile 禁止编译器优化,LOCK_IF_MP 是一个内联函数
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
os::is_MP 判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。在正式解读这段汇编前,我们来了解下嵌入汇编的基本格式:
asm ( assembler template
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
template就是cmpxchgl %1,(%3),表示汇编模板output operands表示输出操作数,=a对应 eax 寄存器input operand表示输入参数,%1就是exchange_value,%3是dest,%4就是mp,r表示任意寄存器,a还是 eax 寄存器list of clobbered registers就是些额外参数,cc表示编译器 cmpxchgl 的执行将影响到标志寄存器,memory告诉编译器要重新从内存中读取变量的最新值,这点实现了volatile的感觉
那么表达式其实就是 cmpxchgl exchange_value,dest,我们会发现 %2 也就是 compare_value 没有用上,这里就要分析 cmpxchgl 的语义了。cmpxchgl 末尾 l 表示操作数长度为 4,上面已经知道了。cmpxchgl 会默认比较 eax 寄存器的值即 compare_value和exchange_value 的值,如果相等,就把 dest 的值赋值给 exchange_value,否则,将 exchange_value 赋值给 eax
最终,JDK 通过 CPU 的 cmpxchgl 指令的支持,实现 AtomicInteger 的 CAS 操作的原子性
CAS 的问题
循环时间长开销大
自旋操作会一直占用 CPU 资源,如果 CAS 一直操作不成功,会造成长时间原地自旋,会给 CPU 带来非常大的执行开销
ABA 问题
如果内存地址 V 初次读取的值是 A,并且在准备赋值的时候检查到它的值仍然为 A,那我们就能说它的值没有被其他线程改变过了吗,这是不一定的。如果在这段期间它的值曾经被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的 ABA 问题
Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过控制变量值的版本 stamp 来保证 CAS 的正确性。因此,在使用 CAS 前要考虑清楚 ABA 问题是否会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效
参考: