1. 简介
CAS 全称是 Compare-and-Swap,即比较加交换,假设我们要对内存中的某个地址进行 CAS 操作,该地址当前值是 V,我们给定预期值 A 和新值 B,如果 V == A,那么就将该地址的值更新为 B 并返回 true
,否则什么也不做只返回 false
,不过这些步骤必须要原子地进行;从 CAS 的执行过程我们可以看到,CAS 的本质就是原子地对某一个内存地址先进行一致性检查,如果一致性检查通过,则执行写入操作,返回成功,如果一致性检查不通过,则返回失败,通知调用方,该内存地址发生了冲突。CAS 是如何保证操作时原子的呢,这是由底层处理器的原子原语来保证的,因此能否执行 CAS 操作,需要 CPU 的支持,当然现代的处理器都时支持 CAS 操作的,处理器提供的 CMPXCHG 指令就是执行 CAS 操作的。Java 对 CAS 的支持是通过 sun.misc.Unsafe
类来支持的,Unsafe
内部提供了本地方法用于对内存中的某一个地址执行 CAS 操作。
2. CAS 的优势和局限性
CAS的优势
CAS 支持对某一个内存地址进行原子的检查 + 更新的复合操作,在很多场合下,可以代替系统互斥量和信号量,减少应用程序的系统调用,并且可以大大减少锁的使用,无锁的非阻塞同步算法就是基于 CAS 进行设计和实现的,这些非阻塞同步算法能够大大提升系统的并发性能和可伸缩性,并且能够很好地防止活跃性故障的发生。
CAS的局限性
- CAS 只能对一个内存地址的值进行单一的检查 + 改写的原子操作,因此使用 CAS 进行同步算法设计非常复杂,需要引入复杂的数据结构,这些复杂性会带来很多额外的开销,并且极易出错;
- CAS 操作很难将多个对像的多个调用组合成一次原子调用,随着需要同步的变量数增加,使用 CAS 来设计无锁同步算法的复杂度就会指数级上升,最终变得不可能。因此,CAS 适合应用于共享变量很少的场合。
- 使用 CAS 设计的同步算法可能带来 ABA 问题,为了解决 ABA 问题会引入额外的性能开销。注意并非 CAS 本身具有ABA问题,而是基于 CAS 设计的同步算法。
ABA问题
下面的代码是 CAS 的典型应用模式,在第 7 行,将原子变量 v
的值保存在本地的副本 oldVal
上面,用于后面做一致性检查,第 8 行计算新值,第 9 行执行 CAS 操作,如果在执行 CAS 操作的时候发现 v
的值与本地的副本 oldVal
不相等,那么说明检测到了冲突,有其他线程正在并发调用 calac
方法,那么就要进行失败重试,但是有一种情况,也发生了冲突,但是却能够通过一致性检查,比如当前线程执行完第7行(此时 v
的值是 A,oldVal
的值也是 A)但还未执行第9行时,有1个线程成功将 v
值从 A 改成了 B,而另外一个线程又成功将 v
的值从 B 改回 A,v
的值发生 A -> B -> A 的变化,这时当前线程执行到第9行,因为 v
的当前值等于其副本 oldVal
,所以成功通过了一致性检查,成功更新了 v
的值,这时候就发生了 ABA 错误,ABA 错误导致冲突检查机制失效,当前线程并未检测到实际已经发生了的冲突。
1 -> public class Demo {
2 ->
3 -> private final AtomicInteger v = new AtomicInteger();
4 ->
5 -> public void calac() {
6 -> while (true) {
7 -> int oldVal = v.get();
8 -> int newVal = calacNewVal(oldVal);
9 -> if(v.compareAndSet(oldVal, newVal)) {
10 -> break;
11 -> }
12 -> }
13 -> }
14 ->
15 -> int calacNewVal(int oldVal) {
16 -> ...
17 -> }
18 -> }
ABA 问题并不是一定会发生,当原子变量的值不会重复时,比如变量的值是单调增加或单调递减的,这种情况下就不会出现 ABA 问题。一般我们使用原子变量是用来做同步计数器,因此它的值是递增的,所以当使用原子变量做计数器时无需担心 ABA 问题,但是对于原子引用类 AtomicReference
,因为它的值(引用地址)是完全随机的,而且 AtomicReference
一般应用于各种无锁的链式数据结构中,因此需要考虑解决 ABA 问题,JDK 也提供了带有版本戳的原子引用类 AtomicStampedReference
,能够避免 AtomicReference
的 ABA 问题,但同时也带来了额外的存储空间消耗和性能开销。