CAS 全称是 CompareAndSwap,即:比较并替换。在 Java 中的 Unsafe 类中有对其包装的方法。
1. CAS
CAS 底层是在 CPU 层面实现的,依赖 MESI(缓存一致性) 协议,缓存行的四种状态:Modified(已修改)、Exclusive(独占)、Shared(共享)、Invalid(失效)。多核 CPU 中,缓存行都处于这四种状态之一。
更多 MESI 详阅:计算机组成原理_CPU缓存
MESI 流程如下:
假设 Core1、Core2 都持有相同地址对应的缓存行,并且它们各自的状态都是 Shared(共享)。
如果此时 Core1 想要成功修改,首先要把缓存行的状态由 Shared(共享)改为 Exclusive(独占),即 Core1 向总线广播 Exclusive(独占)状态请求,获取当前对应缓存行数据的所有权,Core2 嗅探到Exclusive(独占)请求,会将自己 Core 所属的 cache line(缓存行)状态设置为失效(Invalid)。
但如果此时 Core1、Core2 同时向总线广播 Exclusive(独占)状态请求,那就会触发总线仲裁,看 Core1、Core2 谁能获胜,获胜者就能获取 cache line(缓存行)的独占;失败者则将自己 Core 所属的 cache line(缓存行)状态设置为失效(Invalid)。
CAS 操作包含三个操作数:内存位置、预期值、新值。
CAS 的实现逻辑是:将内存位置处的数值与预期值进行比较,如果相等,则将内存位置处的值替换为新值;或不等,则不做任何操作。
直白的说:多个 Core 同时执行针对同一地址的CAS指令时,其实是在试图获取缓存行的独占权。
在多核 CPU 中,通过将 CMPXCHG 指令与 LOCK 前缀指令结合起来,实现以原子方式执行比较和交换操作。
一般情况下 CAS 操作都会采用自选的方式,也就是说当发生 CAS 冲突而导致失败时,会尝试自旋一定的次数,这种情况下如果并发数高,会导致 CAS 一直在不断地重复执行进入忙等待状态,这个过程非常耗 CPU 性能。
CAS 虽然在应用层面是无锁的,但在 CPU 层面其实是通过 LOCK 前缀指令保证原子性。
2. ABA 问题
有两个核心 Core1 和 Core2,Core1 从内存中读取到的数据为A,在 Core1 进行修改之前,Core2 通过 CAS 将值改为了 B,然后又改回了 A。此时 Core1 继续执行,依旧可以 CAS 成功,但过程中有其他 Core 修改了数据。
常用解决办法是加版本号解决。如 Java 中 AtomicStampedReference 类的 CAS 实现了版本号机制。