深入理解 CAS 与悲观锁、乐观锁

本文将从悲观锁与乐观锁出发,系统梳理 CAS(Compare And Swap)的原理、实现方式及优缺点,帮你打牢“高并发”的基本功。


一、悲观锁与乐观锁的基本概念

1. 悲观锁(Pessimistic Lock)

悲观锁的核心思想是悲观地认为总有人和我抢资源。因此,每次访问共享资源时,必须加锁,只有拿到锁的线程才能执行,否则就阻塞等待。

典型实现:synchronizedReentrantLock

适用场景: 写操作多,冲突概率高,锁粒度不细。

问题: 加锁/解锁的开销 + 线程阻塞/唤醒的系统调度代价。


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. 库存量维护:第一个线程读取库存为1,然后第二个线程扣减了库存(现为0),然后又退货了,给库存再加1,然后第一个线程拿到时间片之后,发现库存还是1,继续操作。
    • 这种情况下,ABA问题其实不需要解决
  2. 只比较值:线程一读取栈顶元素为A,然后线程二做了两次出栈操作,线程一通过CAS一对比,栈顶元素还是A,这个时候线程一将A修改为B,但是修改的那个A已经不是曾经读取的那个A了

CAS的ABA问题核心在于它只进行值的比较,但相同的值未必是同一个数据对象

解决方案:

  • 使用 版本号机制
  • JDK 提供 AtomicStampedReferenceAtomicMarkableReference

四、JDK 中的原子类是如何实现的?

1. AtomicInteger/AtomicLong

底层维护一个 volatile 修饰的值,更新时用 CAS 实现原子操作。

private volatile int value;
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

2. AtomicReference

CAS 操作的是对象引用地址

3. AtomicStampedReference

不仅比较对象值,还比较版本号,从而解决 ABA 问题。


五、数据库中的乐观锁:CAS 思想的延伸

数据库中也能实现乐观锁,比如在库存扣减场景中:

UPDATE product_stock
SET stock = stock - 1
WHERE product_id = 123 AND stock = 当前值;

或者通过增加版本号字段:

UPDATE product_stock
SET stock = stock - 1, version = version + 1
WHERE product_id = 123 AND version = 当前版本;

原子性由数据库内部机制保证,不需要我们操心。

是不是无锁?

  • SQL 语句看起来是无锁;
  • 数据库底层执行 UPDATE 时通常加了行锁(悲观锁);
  • 所以从上层看是乐观锁,从底层看还是有锁的悲观锁

六、CAS + 自旋真的万能吗?

不是。它并不适合:

  • 临界区代码执行时间较长;
  • 高并发失败重试率高的场景。

这时候,Java 的 synchronized 锁会自动升级为重量级锁,走操作系统调度,避免自旋浪费资源。


总结

  • 悲观锁是怕线程冲突,每次都先加锁。
  • 乐观锁是假设没人冲突,出了问题再重试。
  • CAS 是乐观锁的代表,实现靠底层原子指令。
  • 从代码层看它是“无锁”,从硬件看它是“加锁”。
  • 有 ABA 和自旋开销问题,但性能优秀。
  • 了解原理后你才能真正掌握 Atomic、JUC,写出真正高性能的代码。

参考视频

面试官内心os: 一个CAS你都能给我聊出花来??

posted @ 2025-05-22 21:45  Vcats  阅读(98)  评论(0)    收藏  举报