一、什么是 CAS

CAS(CompareAndSwap),中文叫比较交换,一种无锁原子算法(乐观锁)。

  • 过程是这样:它包含 3 个参数 CAS(V,E,N),V:表示要更新变量的值E:表示预期值N:表示新值。仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前 V 的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

  • CAS的全称为 Compare And Swap,直译就是比较交换。是一条 CPU 的原子指令,其作用是让 CPU 先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在 intel 的 CPU 中,使用的是 cmpxchg 指令,就是说 CAS 是靠硬件实现的,从而在硬件层面提升效率。

  • 多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。

  • 与锁相比,使用 CAS 会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。

  • 简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,就说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。

二、CAS 底层原理

这样归功于硬件指令集的发展,实际上,我们可以使用同步的方式将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,让硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:

  • 测试并设置(Tetst-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap)
  • 加载链接/条件存储(Load-Linked/Store-Conditional)

CPU 实现原子指令有 2 种方式:

1、通过总线锁定来保证原子性

总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。

2、通过缓存锁定来保证原子性

所谓 缓存锁定 是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,而是修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效

注意:有两种情况下处理器不会使用缓存锁定。

  1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
  2. 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。

三、CAS 源码分析

  1. public class CasDemo01 {
  2. private static volatile int m = 0;
  3. private static AtomicInteger x = new AtomicInteger(0);
  4. public static void increase1() {
  5. m++;
  6. }
  7. public static void increase2() {
  8. x.incrementAndGet();
  9. }
  10. public static void main(String[] args) throws InterruptedException {
  11. Thread[] t1 = new Thread[20];
  12. for (int i = 0; i < 20; i++) {
  13. t1[i] = new Thread(CasDemo01::increase1);
  14. t1[i].start();
  15. t1[i].join(); // join 起了作用
  16. }
  17. Thread[] t2 = new Thread[20];
  18. for (int i = 0; i < 20; i++) {
  19. t2[i] = new Thread(CasDemo01::increase2);
  20. t2[i].start();
  21. t2[i].join(); // join 起了作用
  22. }
  23. System.out.println("int = " + m);
  24. System.out.println("AtomicInteger = " + x.get());
  25. }
  26. }

反编译:javap -c CasDemo01 > CasDemo01.txt

image.png

  • AtomicInteger 类的相关变量:

image.png

Unsafe 是 CAS 的核心类,Java 无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM 还是开了一个后门:Unsafe 类,它提供了硬件级别的原子操作。

image.png

  • AtomicInteger 类的 incrementAndGet() 方法

image.png

  • 查看调用本地方法的源码:\hotspot\src\share\vm\prims*unsafe.cpp*

**
image.png

  • 查看调用本地 CPU 的汇编代码:\hotspot\src\os_cpu\windows_x86\vm*atomic_windows_x86.inline.hpp*

image.png

incrementAndGet() -> unsafe 类 -> getAndAddInt 方法 -> compareAndSwapInt 本地方法 -> unsafe.cpp -> **atomic_windows_x86.inline.hpp 汇编 -> 最终调用 cmpxchg **指令实现一条语句自增
**

四、CAS 缺点

CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。

1、循环时间太长

如果 CAS 一直不成功呢?这种情况绝对有可能发生,如果自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销。在 JUC 中有些地方就限制了 CAS 自旋的次数,例如 BlockingQueue 的 SynchronousQueue

2、只能保证一个共享变量原子操作

看了 CAS 的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用 CAS 也不错。例如读写锁中 state 的高地位

3、ABA 问题

问题分析

CAS 需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是 A,变成了 B,然后又变成了 A,那么在 CAS 检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA 问题。对于 ABA 问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加 1 即可
即:A -> B -> A,变成:A(1) -> B(2) -> A(3)

image.png

问题改进(版本控制)

image.png

五、CAS 应用场景

  • 应用于简单的数据计算,且需要保证原子性的场景
  • 线程冲突少,且需要保证原子性的场景