第15题:说一下悲观锁和乐观锁的区别?
📚 回答:
- 核心考点 : 悲观锁与乐观锁的区别不是"加锁 vs 不加锁"这么简单,而是两种完全不同的并发控制哲学 。大厂面试不会只问"有什么区别",而是深入考察 冲突概率的量化判断 (什么阈值下切换策略)、实现路径的底层差异 (CPU 指令 vs 应用层版本号)、以及混合策略的工程实践(读乐观 + 写悲观、本地 CAS + 数据库版本号)。面试官真正想判断的是:你是否能建立从硬件到业务的完整认知,并在复杂场景下做出正确选型。
1. 核心思想对比------两种并发控制哲学
| 维度 | 悲观锁(Pessimistic Lock) | 乐观锁(Optimistic Lock) |
|---|---|---|
| 核心假设 | 冲突是常态,先加锁再操作 | 冲突是少数,先操作再检测 |
| 控制时机 | 访问前加锁 | 提交时检测 |
| 阻塞行为 | 其他线程阻塞等待 | 其他线程不阻塞,失败重试 |
| 一致性保障 | 物理阻塞保证 | 版本号/CAS 检测保证 |
| 适用冲突率 | > 30% | < 20% |
| 心智模型 | "先占坑,再办事" | "先办事,冲突了再重来" |
类比理解:
- 悲观锁 = 去银行排队,先到窗口占住位置(加锁),办完才走;
- 乐观锁 = 去银行取号,叫到号时如果发现前面有人插队(版本号变了),重新取号再排。
2. 实现机制对比------从 CPU 到数据库的全链路差异
-
2.1 Java 层面的实现
特性 悲观锁 乐观锁 代表实现 synchronized、ReentrantLockAtomicInteger、LongAdder、StampedLock底层机制 Monitor( _owner+_EntryList)CAS( lock cmpxchg)线程状态 RUNNABLE → BLOCKED/WAITING → RUNNABLE 始终 RUNNABLE(自旋或成功) 上下文切换 有(内核态切换 ~1-10ms) 无(纯用户态 ~10-100ns) 内存开销 Monitor 对象 + 队列节点 无额外对象(除 Atomic包装)代码对比:
java// ========== 悲观锁:synchronized ========== private int count = 0; public synchronized void increment() { count++; // 获取 Monitor → 执行 → 释放 Monitor } // 字节码:monitorenter + getfield + iadd + putfield + monitorexit // 涉及:用户态→内核态切换、线程队列、操作系统调度 // ========== 乐观锁:CAS ========== private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // lock cmpxchg 自旋直到成功 } // 字节码:getfield + loop(getvolatile + cmpxchg) + putfield // 涉及:纯 CPU 指令,无内核态切换 -
2.2 数据库层面的实现
特性 悲观锁 乐观锁 SQL 语法 SELECT ... FOR UPDATEUPDATE ... WHERE version = ?锁类型 行锁 / 间隙锁 / 表锁 无锁,版本号检测 隔离级别依赖 依赖 RR/RC 与应用层隔离级别无关 死锁风险 有(需检测和回滚) 无 重试责任 数据库(锁等待) 应用层(版本冲突抛异常) SQL 对比:
sql-- ========== 悲观锁 ========== BEGIN; SELECT stock, version FROM product WHERE id = 1 FOR UPDATE; -- 加行锁 -- 业务计算 UPDATE product SET stock = stock - 1 WHERE id = 1; -- 锁内更新 COMMIT; -- 释放锁 -- ========== 乐观锁 ========== BEGIN; SELECT stock, version FROM product WHERE id = 1; -- 无锁读取 -- 业务计算 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5; -- 提交时检测版本 -- 影响行数 = 0 表示冲突,应用层重试 COMMIT; -
2.3 分布式层面的实现
特性 悲观锁 乐观锁 实现方式 Redis RedLock、ZooKeeper、数据库分布式锁 版本号 + CAS、MVCC、Saga 模式 协调成本 高(需要中心节点) 低(无中心协调) 网络开销 每次加锁/解锁需网络 RTT 提交时一次网络 RTT 典型场景 分布式任务调度、库存扣减 分布式配置、最终一致性事务
3. 性能模型对比------冲突概率决定胜负
-
3.1 理论性能曲线
假设 100 线程并发,执行 100 万次累加:
冲突概率 悲观锁(synchronized) 乐观锁(AtomicLong) 乐观锁(LongAdder) 0% 2.5s 1.2s 1.5s 5% 2.8s 1.5s 1.6s 20% 4.0s 3.5s 2.0s 50% 8.0s 12.0s(自旋风暴) 3.0s 80% 15.0s 60.0s+(活锁) 5.0s 99% 30.0s(串行化) 不可用 8.0s 关键结论:
- 低冲突(< 20%):乐观锁性能碾压悲观锁(无上下文切换);
- 高冲突(> 50%):悲观锁更稳定(线程阻塞释放 CPU),乐观锁自旋导致 CPU 打满;
- 极端冲突(> 90%):两者都退化,需队列化(单线程串行处理)。
-
3.2 延迟分布对比
百分位 悲观锁(P99) 乐观锁(P99) 说明 P50 2ms 0.1μs 乐观锁无阻塞,延迟极低 P90 5ms 0.5μs 悲观锁受线程调度影响 P99 50ms 10ms+ 乐观锁高冲突时重试累积 P99.9 200ms 100ms+ 悲观锁锁等待超时风险
4. 功能特性对比------不仅仅是性能
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 死锁 | 有(需预防/检测) | 无 |
| 写偏斜 | 无(锁范围保护) | 有(需 Serializable 或业务补偿) |
| ABA 问题 | 无 | 有(CAS 路径) |
| 可重入 | 支持(ReentrantLock) |
不支持(CAS 无持有概念) |
| 公平性 | 可配置(公平/非公平) | 天然非公平(随机成功) |
| 条件变量 | 支持(Condition) |
不支持 |
| 超时获取 | 支持(tryLock(timeout)) |
不支持(自旋或立即失败) |
| 中断响应 | 支持(lockInterruptibly) |
不支持 |
| 批量操作 | 容易(锁内多行操作) | 困难(需事务包裹) |
| 跨行一致性 | 容易(锁多行) | 困难(多版本号管理) |
5. 适用场景对比------从业务维度选型
-
5.1 悲观锁的主战场
场景 原因 典型实现 金融转账 强一致性,零容忍数据不一致 SELECT FOR UPDATE+ 事务库存扣减 写冲突高,需串行化 分布式锁 + 数据库行锁 订单状态机 状态流转需严格顺序 synchronized+ 状态校验全局 ID 生成 必须唯一且连续 数据库号段模式(Leaf) 分布式任务调度 同一任务只能一个节点执行 ZooKeeper 分布式锁 -
5.2 乐观锁的主战场
场景 原因 典型实现 商品详情页浏览 读多写少(1000:1) 无锁读取 + 缓存 用户积分查询 低频更新,高频查询 版本号 + 缓存 配置中心读取 几乎无写,海量读 CopyOnWriteArrayList计数器/统计 高并发累加,允许估算 LongAdder分布式配置更新 最终一致性即可 CAS + 版本号 -
5.3 混合策略场景
场景 策略 说明 读写分离系统 读乐观 + 写悲观 读走缓存无锁,写走数据库加锁 秒杀系统 本地 CAS + 数据库乐观锁 Redis 预减(乐观)+ 数据库兜底(悲观) 缓存一致性 乐观更新 + 异步补偿 CAS 更新缓存,MQ 异步同步数据库
6. 工程选型决策树
是否需要强一致性(如金融、库存)?
├── 是 → 悲观锁
│ └── 单机 or 分布式?
│ ├── 单机 → synchronized / ReentrantLock
│ └── 分布式 → Redis RedLock / ZooKeeper / 数据库分布式锁
└── 否 → 冲突概率评估
├── < 5%(读多写少)→ 乐观锁
│ ├── 单机计数 → AtomicLong / LongAdder
│ ├── 数据库更新 → 版本号字段
│ └── 分布式配置 → CAS + 版本号
├── 5%~30%(读写均衡)→ 混合策略
│ ├── 读:无锁 / 乐观锁
│ └── 写:悲观锁 / 队列化
└── > 30%(写多读少)→ 悲观锁 + 优化
├── 锁粒度细化(行锁替代表锁)
├── 读写分离(ReadWriteLock)
└── 队列化串行(Disruptor / 单线程)
7. 常见误区澄清
| 误区 | 正确理解 |
|---|---|
| "乐观锁不加锁,所以一定更快" | ❌ 高冲突下乐观锁自旋导致 CPU 100%,可能比悲观锁更慢 |
| "悲观锁就是 synchronized" | ❌ 悲观锁是思想,synchronized 只是 JVM 实现之一,数据库 FOR UPDATE 也是悲观锁 |
| "乐观锁只能用于数据库" | ❌ Java Atomic 类、StampedLock 都是乐观锁实现 |
| "悲观锁一定保证一致性" | ❌ 未正确使用事务隔离级别或锁粒度,仍可能出现幻读、不可重复读 |
| "高并发必须用乐观锁" | ❌ 秒杀等高冲突场景,乐观锁重试风暴会导致系统崩溃 |
| "乐观锁无死锁" | ✅ 正确,但可能有活锁(无限重试)和饥饿(某些线程一直失败) |
8. 生产环境避坑指南
-
8.1 避免"一刀切"选型
java// ❌ 错误:所有场景都用 synchronized public class BadDesign { private Map<String, Config> configs = new HashMap<>(); public synchronized Config getConfig(String key) { return configs.get(key); // 读操作也加锁! } } // ✅ 正确:读用乐观,写用悲观 public class GoodDesign { private volatile Map<String, Config> configs = new HashMap<>(); public Config getConfig(String key) { return configs.get(key); // 读:无锁,volatile 保证可见性 } public synchronized void updateConfig(String key, Config config) { Map<String, Config> newConfigs = new HashMap<>(configs); newConfigs.put(key, config); configs = newConfigs; // 写:CopyOnWrite 思想 } } -
8.2 监控冲突率,动态调整策略
java// 埋点监控乐观锁冲突率 Counter conflictCounter = meterRegistry.counter("optimistic.lock.conflict"); public boolean updateWithVersion(Product product) { int rows = productDao.update(product); if (rows == 0) { conflictCounter.increment(); // 冲突率 > 20% 时告警,提示改用悲观锁 return false; } return true; } -
8.3 数据库乐观锁必须配合索引
sql-- ❌ 错误:version 无索引,全表扫描 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5; -- id 是主键,OK -- ❌ 危险:按非索引字段更新 UPDATE product SET stock = stock - 1, version = version + 1 WHERE sku = 'ABC123' AND version = 5; -- sku 无索引 → 全表扫描 + 表锁! -
8.4 分布式锁必须设置超时
java// ❌ 错误:无超时,死锁后永久阻塞 lock.lock(); // ✅ 正确:超时 + 看门狗续期 if (lock.tryLock(10, TimeUnit.SECONDS)) { try { /* 业务 */ } finally { lock.unlock(); } }
9. 面试官追问与高分回答模板
-
追问 1:"悲观锁和乐观锁的区别是什么?"
- 低分回答:"悲观锁加锁,乐观锁不加锁用版本号。"(太浅,没有触及本质)
- 高分回答 : "悲观锁和乐观锁是两种完全不同的并发控制哲学:
- 核心假设:悲观锁假设冲突是常态,先加锁再操作;乐观锁假设冲突是少数,先操作再检测。
- 实现机制:悲观锁通过物理阻塞(Monitor、数据库行锁)保证独占;乐观锁通过冲突检测(CAS、版本号)保证一致性。
- 性能特征:低冲突时乐观锁性能碾压(无上下文切换),高冲突时悲观锁更稳定(线程阻塞释放 CPU)。
- 功能差异:悲观锁支持可重入、条件变量、超时获取;乐观锁天然非公平、无阻塞、无死锁但可能有活锁。
选型的唯一金标准是冲突概率:< 20% 用乐观锁,> 30% 用悲观锁,中间地带用混合策略。"
-
追问 2:"什么场景下乐观锁比悲观锁慢?"
- 高分回答 : "高冲突场景(> 50%)下乐观锁会比悲观锁慢,原因有三:
- 自旋风暴:CAS 失败率高时,线程 100% CPU 空转,但业务吞吐量几乎为 0;
- 缓存行竞争:多核同时 CAS 同一变量,缓存行在核心间频繁'乒乓',总线饱和;
- 活锁:所有线程同时读取、同时 CAS、同时失败,循环往复。
量化数据:100 线程并发,冲突率 80% 时,AtomicLong 的吞吐量可能只有 synchronized 的 1/10,且 CPU 使用率 100%。此时悲观锁的线程阻塞反而释放了 CPU 资源,整体吞吐量更高。"
- 高分回答 : "高冲突场景(> 50%)下乐观锁会比悲观锁慢,原因有三:
-
追问 3:"数据库中悲观锁和乐观锁怎么选?"
- 高分回答 : "数据库层面的选型同样取决于冲突概率和一致性要求:
- 读多写少(< 5% 冲突) :乐观锁(版本号)。例如商品详情页,读:写 = 1000:1,加
FOR UPDATE会阻塞大量读线程; - 读写均衡(5%~30%) :混合策略。读走主从复制(无锁),写走悲观锁(
FOR UPDATE)或乐观锁 + 重试; - 写多读少(> 30%):悲观锁。例如库存扣减,冲突率高,乐观锁重试风暴会导致数据库 CPU 打满;
- 强一致性(金融转账):悲观锁 + Serializable 隔离级别,或分布式事务(Seata XA)。
关键细节:乐观锁的
UPDATE ... WHERE version = ?必须确保 WHERE 条件走索引,否则退化为全表扫描,效果等同于表锁。" - 读多写少(< 5% 冲突) :乐观锁(版本号)。例如商品详情页,读:写 = 1000:1,加
- 高分回答 : "数据库层面的选型同样取决于冲突概率和一致性要求:
-
追问 4:"乐观锁的 ABA 问题,悲观锁有吗?"
- 高分回答 : "悲观锁没有 ABA 问题,因为悲观锁通过物理阻塞确保操作期间没有其他线程修改数据。
乐观锁的 ABA 问题分两种实现:
- CAS 路径 :
AtomicReference存在 ABA,因为 CAS 只比较值,不比较修改历史。解决用AtomicStampedReference(版本号); - 版本号路径 :数据库乐观锁的
version字段递增,天然解决 ABA(值回退但版本号不同)。
所以 ABA 是 CAS 特有的问题,版本号乐观锁和悲观锁都不存在。"
- CAS 路径 :
- 高分回答 : "悲观锁没有 ABA 问题,因为悲观锁通过物理阻塞确保操作期间没有其他线程修改数据。
-
追问 5:"分布式环境下,悲观锁和乐观锁怎么选?"
- 高分回答 : "分布式环境下,两者的实现和权衡都发生了变化:
悲观锁的分布式化:
- 单机
synchronized失效,需引入 Redis RedLock、ZooKeeper 分布式锁; - 代价:网络 RTT(~1-5ms)、时钟漂移风险(RedLock)、脑裂风险;
- 适用:必须强互斥的场景(如分布式任务调度、全局 ID 生成)。
乐观锁的分布式化:
- 数据库版本号天然支持分布式(无中心协调);
- 代价:冲突检测在提交时,网络往返后才发现冲突,重试成本更高;
- 适用:最终一致性场景(如配置更新、缓存同步)。
现代趋势:分布式场景下,两者都在向'无锁化'演进------Saga 模式(最终一致性)、CRDT(无锁数据结构)、MVCC(多版本并发控制)正在替代传统的锁方案。"
- 单机
- 高分回答 : "分布式环境下,两者的实现和权衡都发生了变化:
-
追问 6:"如果让你设计一个秒杀系统,你会怎么选锁?"
- 高分回答 : "秒杀系统是'极端高冲突'场景(万人抢 100 件商品,冲突率 99.99%),传统锁方案都不适用,需要分层解耦:
- 流量层:Nginx + Lua 限流,99% 请求直接拒绝,只剩 1% 进入后端;
- 缓存层 :Redis
decr预减库存(原子操作,无锁),库存为 0 直接返回'已售罄'; - 消息队列:通过 MQ 异步下单,队列化串行处理,彻底消除并发冲突;
- 数据库层:最终一致性写入,用乐观锁(版本号)兜底,冲突率已极低;
- 降级策略:Redis 降级为本地缓存,MQ 降级为直接写库 + 悲观锁(最后防线)。
核心思想:不是'选哪种锁',而是'让冲突不要发生'。通过限流、缓存、队列化三层过滤,将数据库层面的冲突率从 99.99% 降到 < 1%,此时乐观锁轻松应对。"
- 高分回答 : "秒杀系统是'极端高冲突'场景(万人抢 100 件商品,冲突率 99.99%),传统锁方案都不适用,需要分层解耦:
10. 方案选型速查表
| 场景 | 冲突率 | 一致性 | 推荐方案 | 不推荐方案 |
|---|---|---|---|---|
| 商品详情页浏览 | < 1% | 最终一致 | 无锁 + 缓存 | 任何锁 |
| 用户配置读取 | < 1% | 最终一致 | CopyOnWriteArrayList |
读写锁 |
| 账户余额查询 | < 5% | 强一致 | 乐观锁(版本号) | 悲观锁 |
| 库存查询 | < 5% | 强一致 | 乐观锁(版本号) | 悲观锁 |
| 积分累加 | 5%~20% | 最终一致 | LongAdder + 异步落库 |
AtomicLong |
| 订单创建 | 20%~50% | 强一致 | 悲观锁(行锁) | 纯乐观锁 |
| 库存扣减(普通) | 30%~50% | 强一致 | 悲观锁 + 索引优化 | 乐观锁 |
| 秒杀库存扣减 | > 99% | 强一致 | Redis 预减 + MQ 队列化 | 任何数据库锁 |
| 金融转账 | 任意 | 强一致 | 悲观锁 + 事务 | 乐观锁 |
| 分布式任务调度 | 低 | 强一致 | ZooKeeper 分布式锁 | Redis 锁 |
| 全局配置更新 | 极低 | 最终一致 | CAS + 版本号 | 分布式锁 |
💡 面试官想要的满分总结:
悲观锁与乐观锁的区别不是"加锁 vs 不加锁",而是两种并发控制哲学的根本分歧:悲观锁"先占坑再办事",用物理阻塞保证强一致;乐观锁"先办事再检查",用冲突检测换取高并发。
选型的唯一金标准是冲突概率:< 20% 时乐观锁性能碾压(无上下文切换、无内核态切换),> 30% 时悲观锁更稳定(线程阻塞释放 CPU、避免自旋风暴)。但两者都不是银弹------极端冲突下(> 90%),任何锁都会退化,必须通过限流、缓存、队列化从根本上消除冲突。
工程实践中,混合策略 是主流:读多写少场景读走乐观/无锁、写走悲观;秒杀等高冲突场景通过 Redis 预减 + MQ 队列化将数据库冲突率降到 < 1%;分布式环境下,Saga 模式和 MVCC 正在替代传统锁方案。最后记住:最好的锁是不用锁------通过架构设计让冲突不要发生,比选哪种锁更重要。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯