浅谈 CAS

·  阅读 40

drew-beamer-Vc1pJfvoQvY-unsplash.jpg

基于openJDK8分析

1. 什么是CAS ?

CAS 全称是 compare and swap,翻译过来就是比较并交换,这也是它的核心,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个角色 --- 内存地址(我们不用管),旧的预期值A,要修改的新值B。在正式修改变量之前,它要将预期值A于在相应内存地址的实际值进行比较,如果相等,则将新值B替换到内存地址的实际值,如果不相等,则将此时的实际值作为新的预期值,然后再循环。 在 Java 中,Java 并没有直接实现 CAS,而是通过 C++ 内联汇编的形式实现的。Java 代码需通过 JNI 才能调用。

CAS 是一条 CPU 的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。

我们可以通过一张图来梳理CAS的过程:

CAS.jpg

2. CAS 的产生

在修饰共享变量的时候经常使用volatile关键字,但是volatile仅可以保证可见性和禁止指令重排(有序性),无法保证原子性。虽然在单线程中没有问题,但是多线程就会出现各种问题,造成现场不安全的现象。所以jdk1.5后产生了CAS利用CPU原语(不可分割,连续不中断)保证线程操作原子性。

3. CAS 的底层分析

在JDK1.5 中新增java.util.concurrent.atomic(JUC)就是建立在CAS之上的。相对于对于synchronized这种锁机制,CAS是非阻塞算法的一种常见实现。所以JUC在性能上有了很大的提升。

  1. 基本类型的:AtomicInteger, AtomicBoolean, AtomicLong
  2. 数组类型的:AtomicIntegerArray, AtomicLongArray,AtomicReferenceArray
  3. 引用类型的:AtomicReference, AtomicMarkableReference, AtomicStampedReference

AtomicInteger 是我们日常开发中经常使用的一个原子类了,那么我们就通过它来分析下底层原理

public class AtomicTest {
    public static void main(String[] args) {

        AtomicInteger atomicInteger = new AtomicInteger();
        //TODO:自增
        atomicInteger.incrementAndGet();
    }
}
复制代码

进一步分析:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
复制代码

不难发现,它内部是通过 Unsafe 这个类实现的。

那么继续跟进去:

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;
}
复制代码

可以看到他是一个 do-while 的循环结构,其中 compareAndSwapInt 方法就是我们的CAS了,继续点进去看下:

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

可以看到,这是一个native方法,底层是由C/C++h实现的,所以我们需要进入到openJDK中去查看

unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  //TODO:由JNI调用
  oop p = JNIHandles::resolve(obj);
  //TODO: 根据偏移量,计算 value 的地址。这里的 offset 就是 AtomaicInteger 中的 valueOffset
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  //TODO:核心就是 Atomic::comxchg
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
复制代码

继续往下看:在 atomic.inline.hpp 文件中定义了不同平台加载的文件,我们以 linux_x86 进行分析:

//TODO:内联方法
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;
}
复制代码

Atomic::cmpxchg 方法的定义如上图所示,它首先通过 os::is_MP() 判断当前执行环境是否为多处理器环境(multi processors),然后嵌入一段汇编代码(—asm- 这一行),这段汇编代码会执行一条 cmpxchgl 指令,同时把 exchange_value 等变量作为操作数,当它执行完成之后,方法将直接返回 exchange_value 的值。

汇编语言是一种可以直接对CPU操作的机器语言,是一种最底层的语言。

从中可以看出, cmpxchgl 汇编指令是整个 Atomic::cmpxchg 方法的核心。(linux_x86)

顺便补充一下,汇编代码中的 LOCK_IF_MP 是一个宏,这个宏的作用是,在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果。内存屏障能够在目标指令执行之前,保障多个处理器之间的缓存一致性,由于单处理器环境下并不需要内存屏障,故做此判断。 我们简单看下,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: " // 就是添加lock 前缀
复制代码

所以,CAS的核心就是(linux_x86):

lock cmpxchgl 指令
复制代码

所以这段汇编代码翻译过来就是:

 __asm__ volatile ("lock cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
复制代码

cmpxchgl 指令是包含在 linux_x86 架构及 IA-64 架构中的一个原子条件指令,在我们的例子中,它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给 exchange_value。这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。

事实上,现代指令集架构基本上都会提供 CAS 指令,例如 x86 和 IA-64 架构中的 cmpxchgl 指令和 comxchgq 指令,sparc 架构中的 cas 指令和 casx 指令, 以及 windows架构中的 cmpxchg 指令等等。

不管是 Hotspot 中的 Atomic::cmpxchg 方法,还是 Java 中的 compareAndSwapInt 方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。

4. CAS 的优缺点

4.1 优点

CAS是一种乐观锁的思想,而且是一种非阻塞的轻量级的乐观锁,非阻塞式是指一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

4.2 缺点

  1. 循环时间长开销大

CAS一般搭配死循环做自旋操作(不成功,就一直循环执行,直到成功),如果长时间不成功,则会一直占用CPU资源,这也是一个非常大的开销。

  1. 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用CAS 的方式来保证原子操作,但是对多个共享变量操作时,CAS 就无法保证操作的原子性,这个时候就可以用锁,在JDK1.5中提供了 AtomicReference 类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行 CAS 操作。

  1. 经典的ABA问题 谈到 CAS,基本上都要谈一下 CAS 的 ABA 问题。CAS 由三个步骤组成,分别是“读取-比较-写回”

举个例子,线程t1从内存中读取出原值i=5,此时线程切换到t2,t2从内存中读取出原值也是5,然后CAS操作,将i修改为10,此时切换到线程t3,t3从内存中读取出原值i=10,然后线程t3执行CAS操作,将i重新写会5,此时线程切换到了t1, t1执行比较操作,发现值一样,都是5,然后将i修改为8,写回到内存中。对于线程t1来说,这就是一个ABA的问题,其他线程修改了数次,但是最后修改值和原值一样。

对于ABA问题,如果只关注结果,那么它就不是个问题!

如何解决ABA问题呢?

ABA问题的解决思路其实也很简单,就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A了。 java.util.concurrent.atomic 包下提供了一个可处理 ABA 问题的原子类AtomicStampedReference。其中 AtomicMarkableReference 类也可以解决ABA的问题,只不过它是通过boolean 值打标的方式来确实是否被修改过,只不过无法统计被修改的次数。

AtomicStampedReference类的compareAndSet方法会首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值 image.png

5. CAS 的使用时机

  1. 线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效。
  2. 线程数较大、等待时间长,不建议使用自旋锁,占用CPU较高

6. 总结

  1. CAS 底层是一个硬件原语,硬件直接支持,具有天然的原子性,Java的CAS也只是硬件原语的封装。
  2. 以linux_x86为例,CAS等价于:lock cmpxchgl 指令 参考文档

限于作者个人水平,文中难免有错误之处,欢迎指正! 勿喷,感谢

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改