如何深入理解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 实现原子操作。
阅读全文