CAS 的全称是:比较并交换(Compare And Swap)。
在 CAS 中,有这样三个值:
- V:要更新的变量(var)
- E:预期值(expected)
- N:新值(new)
比较并交换的过程如下:
判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新,什么都不做。所以这里的预期值 E 本质上指的是“旧值”。
当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
java 中的 CAS
sun.misc
包下的 Unsafe
类提供了 compareAndSwapObject
、compareAndSwapInt
、compareAndSwapLong
方法来实现的对 Object
、int
、long
类型的 CAS 操作。
而这些方法都是直接调用的 native
方法,Java 语言并没有直接用 Java 实现 CAS,Java 中 CAS 是 C++ 内联汇编的形式实现的,通过 JNI(Java Native Interface) 调用。因此,CAS 的具体实现与操作系统以及 CPU 密切相关。
CAS 操作可能会因为并发冲突而失败,因此通常会与 while
循环搭配使用,在失败后不断重试,直到操作成功。这就是 自旋锁机制 。
1 | /** |
CAS 相关问题
ABA 问题
一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。
Java 中就是会首先检查当前引用是否等于预期引用,再去检查值是否相同,如果都相同,则再去更新值。
循环时间开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
解决思路是让 JVM 支持处理器提供的 pause 指令。
pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。
只能保证一个共享变量的原子操作
CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了 AtomicReference
类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用 AtomicReference
来执行 CAS 操作。
还可以使用锁来保证多个变量的更新的原子性。