CAS( Compare-And-Swap)”比较并交换”(乐观锁的原理)

CAS 的特点是避免使用互斥锁,当多个线程同时使用 CAS 更新同一个变量时,只有其中一个线程能够操作成功,而其他线程都会更新失败。更新失败的线程并不会被阻塞,而是被告知这次由于竞争而导致的操作失败,但还可以再次尝试。


CAS 的思路

CAS 相关的指令是具备原子性的

CAS 有三个操作数:内存值 V、预期值 A、要修改的值 B。

核心思路:仅当预期值 A 和当前的内存值 V 相同时,才将内存值修改为 B。

下面我们用图解和例子的方式,让 CAS 的过程变得更加清晰,如下图所示:

img

假设有两个线程,分别使用两个 CPU,它们都想利用 CAS 来改变右边的变量的值。我们先来看线程 1,它使用 CPU 1,假设它先执行,它期望当前的值是 100,并且想将其改成 150。在执行的时候,它会去检查当前的值是不是 100,发现真的是 100,所以可以改动成功,而当改完之后,右边的值就会从 100 变成 150。

img

如上图所示,假设现在才刚刚轮到线程 2 所使用的 CPU 2 来执行,它想要把这个值从 100 改成 200,所以它也希望当前值是 100,可实际上当前值是 150,所以它会发现当前值不是自己期望的值,所以并不会真正的去继续把 100 改成 200,也就是说整个操作都是没有效果的,此次没有修改成功,CAS 操作失败。

当然,接下来线程 2 还可以有其他的操作,这需要根据业务需求来决定,比如重试、报错或者干脆跳过执行。举一个例子,在秒杀场景下,多个线程同时执行秒杀,只要有一个执行成功就够了,剩下的线程当发现自己 CAS 失败了,其实说明兄弟线程执行成功了,也就没有必要继续执行了,这就是跳过操作。所以业务逻辑不同,就会有不同的处理方法,但无论后续怎么处理,之前的那一次 CAS 操作是已经失败了的。


CAS 和乐观锁的关系,什么时候会用到 CAS?

CAS 在并发容器中的使用情况

1、ConcurrentHashMap中的casTabAt方法

2、ConcurrentLinkedQueue(非阻塞并发队列)的 offer 方法

CAS 在数据库中的使用情况

利用 version 字段在数据库中实现乐观锁和 CAS 操作,而在获取和修改数据时都不需要加悲观锁。

CAS 在原子类中的使用情况


CAS 有什么缺点?

1、ABA 问题。

假设第一个线程拿到的初始值是 100,然后进行计算,在计算的过程中,有第二个线程把初始值改为了 200,然后紧接着又有第三个线程把 200 改回了 100。等到第一个线程计算完毕去执行 CAS 的时候,它会比较当前的值是不是等于最开始拿到的初始值 100,此时会发现确实是等于 100,所以线程一就认为在此期间值没有被修改过,就理所当然的把这个 100 改成刚刚计算出来的新值,但实际上,在此过程中已经有其他线程把这个值修改过了,这样就会发生 ABA 问题。

添加一个版本号 , A→B→A 变成了 1A→2B→3A,这样一来,就可以通过对比版本号来判断值是否变化过,解决 ABA 的问题。

2、自旋时间过长

3、范围不能灵活控制