如何深入理解CAS与悲观锁、乐观锁之间的区别与联系?
摘要:本文将从悲观锁与乐观锁出发,系统梳理 CAS(Compare And Swap)的原理、实现方式及优缺点,帮你打牢“高并发”的基本功。
本文将从悲观锁与乐观锁出发,系统梳理 CAS(Compare And Swap)的原理、实现方式及优缺点,帮你打牢“高并发”的基本功。
一、悲观锁与乐观锁的基本概念
1. 悲观锁(Pessimistic Lock)
悲观锁的核心思想是悲观地认为总有人和我抢资源。因此,每次访问共享资源时,必须加锁,只有拿到锁的线程才能执行,否则就阻塞等待。
典型实现:synchronized、ReentrantLock
适用场景: 写操作多,冲突概率高,锁粒度不细。
问题: 加锁/解锁的开销 + 线程阻塞/唤醒的系统调度代价。
2. 乐观锁(Optimistic Lock)
乐观锁则假设没人和我抢资源,在访问共享资源时不加锁,而是通过检查数据状态是否在此期间被其他线程修改过,如果没有,则修改;否则重试。
典型实现: CAS(Compare And Swap)
适用场景: 读多写少,冲突概率低,代码块短。
二、CAS 的基本原理与实现
1. CAS 是什么?
CAS:比较并交换(Compare And Swap)
举个例子:你和另一个线程想进入“卫生间”,门口有块牌子:
值为 0:空闲
值为 1:有人
两个线程同时看到是 0:
线程 A 快一步把牌子改为 1,进入房间。
线程 B 跑过去一看,变成了 1,进不去。
这块牌子就是状态值。CAS 就是:当状态值等于旧值(预期值)时,才将其更新为新值。
三步核心流程:
if (当前值 == 预期值) {
更新为新值;
返回成功;
} else {
返回失败;
}
2. CAS的原子性问题
先比较再交换,这是两步操作,因此需要保证原子性
怎么保证这两步操作原子性呢?加锁?那就变成了为了不加锁而加锁了
事实上,CAS底层依赖 CPU 的原子指令(如 x86 架构下的 CMPXCHG 指令)来保证操作的原子性。
该指令由硬件直接支持:执行的时候通过锁定总线/锁定缓存行,避免其他CPU访问共享资源,
3. Java 中 CAS 的实现方式
Java 中的 CAS 操作是通过 Unsafe 类提供的:
compareAndSwapInt(Object obj, long offset, int expected, int newValue);
这不是普通 Java 代码,而是 native 方法,在 JVM 层调用 C++ 实现,底层依赖 CPU 的原子指令
4. CAS 真的是“无锁”吗?
表面上看,CAS 没有使用 synchronized 等显式锁机制,从 Java 层面是“无锁”的。
但底层依赖的 CPU 指令,比如“总线锁”、“缓存行锁”,本质上是硬件加锁机制。所以说:
从软件角度:CAS 是无锁的。没有用操作系统的mutex原语,避免了软件层面的所竞争和线程阻塞问题。
从硬件角度:CAS 是锁的。但是硬件直接支持,低开销、不会阻塞线程。
三、CAS 的优缺点与典型问题
优点
无线程阻塞,性能高。
避免操作系统调度,响应快。
缺点
1. 自旋消耗 CPU
失败时不会挂起,而是原地自旋重试,高并发下浪费大量 CPU。
解决办法:
控制重试次数;
结合 synchronized:比如 JDK 的 synchronized 锁,在自旋失败后自动升级为重量级锁。
2. ABA 问题
线程 A 读取值为 A,准备更新为 C,期间线程 B 把值改为 B 再改回 A。线程 A 发现还是 A,误以为未被修改,执行更新,结果出错。
示例:
库存量维护:第一个线程读取库存为1,然后第二个线程扣减了库存(现为0),然后又退货了,给库存再加1,然后第一个线程拿到时间片之后,发现库存还是1,继续操作。
这种情况下,ABA问题其实不需要解决
只比较值:线程一读取栈顶元素为A,然后线程二做了两次出栈操作,线程一通过CAS一对比,栈顶元素还是A,这个时候线程一将A修改为B,但是修改的那个A已经不是曾经读取的那个A了
CAS的ABA问题核心在于它只进行值的比较,但相同的值未必是同一个数据对象
解决方案:
使用 版本号机制;
JDK 提供 AtomicStampedReference、AtomicMarkableReference。
四、JDK 中的原子类是如何实现的?
1. AtomicInteger/AtomicLong
底层维护一个 volatile 修饰的值,更新时用 CAS 实现原子操作。
