CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS操作是一种通过硬件实现并发安全的常用技术,底层通过利用CPU的CAS指令缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V是内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。

当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会自旋,一定次数后若仍未成功则阻塞线程。

底层实现

进入OpenJDK的源码查看c++的代码,代码跟踪顺序是:unsafe.cpp、atomic.cpp,接下来会根据操作系统和处理器的不同来选择对应的调用代码,这里以Windows和x86处理器为例进入atomic_window_x86.inline.hpp,重要代码片段如下:

1
2
3
4
5
6
7
8
9
10
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::isMP(); // 判断处理器的类型是否是多处理器
_asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}

从上面代码可以看到,如果是多处理器,将会为cmpxchg指令添加lock前缀,否则不添加(单处理器自身会维护执行顺序)。对于lock前缀,下面是intel手册的说明:

  • 确保对内存读改写操作的原子执行。 在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性。缓存锁定将大大降低lock前缀指令的执行开销。
  • 禁止该指令,与前面和后面的读写指令重排序。
  • 把写缓冲区的所有数据刷新到内存中。

也就是说,如果是多处理器,通过带lock前缀的cmpxchg指令对缓存加锁(缓存一致性协议)或总线加锁的方式来实现多处理器之间的原子操作;

如果是单处理器,通过cmpxchg指令完成原子操作。

  • 通过缓存锁定来保证原子性
    • 所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当它执行操作写回到内存时,处理器不在总线上输出 LOCK# 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同)。
      • Lock前缀的指令会引起处理器缓存写回内存;
      • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
      • 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。