【大白话说Java面试题 第115题】【并发篇】第15题:说一下悲观锁和乐观锁的区别?
异常处理Java开发基于Spring Boot的异常处理框架设计:电商系统业务异常建模与全局统一响应实现第15题说一下悲观锁和乐观锁的区别回答核心考点 悲观锁与乐观锁的区别不是加锁 vs 不加锁这么简单而是两种完全不同的并发控制哲学。大厂面试不会只问有什么区别而是深入考察冲突概率的量化判断什么阈值下切换策略、实现路径的底层差异CPU 指令 vs 应用层版本号、以及混合策略的工程实践读乐观 写悲观、本地 CAS 数据库版本号。面试官真正想判断的是你是否能建立从硬件到业务的完整认知并在复杂场景下做出正确选型。1. 核心思想对比——两种并发控制哲学维度悲观锁Pessimistic Lock乐观锁Optimistic Lock核心假设冲突是常态先加锁再操作冲突是少数先操作再检测控制时机访问前加锁提交时检测阻塞行为其他线程阻塞等待其他线程不阻塞失败重试一致性保障物理阻塞保证版本号/CAS 检测保证适用冲突率 30% 20%心智模型“先占坑再办事”“先办事冲突了再重来”类比理解悲观锁 去银行排队先到窗口占住位置加锁办完才走乐观锁 去银行取号叫到号时如果发现前面有人插队版本号变了重新取号再排。2. 实现机制对比——从 CPU 到数据库的全链路差异2.1 Java 层面的实现特性悲观锁乐观锁代表实现synchronized、ReentrantLockAtomicInteger、LongAdder、StampedLock底层机制Monitor_owner_EntryListCASlock cmpxchg线程状态RUNNABLE → BLOCKED/WAITING → RUNNABLE始终 RUNNABLE自旋或成功上下文切换有内核态切换 ~1-10ms无纯用户态 ~10-100ns内存开销Monitor 对象 队列节点无额外对象除Atomic包装代码对比// 悲观锁synchronized privateintcount0;publicsynchronizedvoidincrement(){count;// 获取 Monitor → 执行 → 释放 Monitor}// 字节码monitorenter getfield iadd putfield monitorexit// 涉及用户态→内核态切换、线程队列、操作系统调度// 乐观锁CAS privateAtomicIntegercountnewAtomicInteger(0);publicvoidincrement(){count.incrementAndGet();// lock cmpxchg 自旋直到成功}// 字节码getfield loop(getvolatile cmpxchg) putfield// 涉及纯 CPU 指令无内核态切换2.2 数据库层面的实现特性悲观锁乐观锁SQL 语法SELECT ... FOR UPDATEUPDATE ... WHERE version ?锁类型行锁 / 间隙锁 / 表锁无锁版本号检测隔离级别依赖依赖 RR/RC与应用层隔离级别无关死锁风险有需检测和回滚无重试责任数据库锁等待应用层版本冲突抛异常SQL 对比-- 悲观锁 BEGIN;SELECTstock,versionFROMproductWHEREid1FORUPDATE;-- 加行锁-- 业务计算UPDATEproductSETstockstock-1WHEREid1;-- 锁内更新COMMIT;-- 释放锁-- 乐观锁 BEGIN;SELECTstock,versionFROMproductWHEREid1;-- 无锁读取-- 业务计算UPDATEproductSETstockstock-1,versionversion1WHEREid1ANDversion5;-- 提交时检测版本-- 影响行数 0 表示冲突应用层重试COMMIT;2.3 分布式层面的实现特性悲观锁乐观锁实现方式Redis RedLock、ZooKeeper、数据库分布式锁版本号 CAS、MVCC、Saga 模式协调成本高需要中心节点低无中心协调网络开销每次加锁/解锁需网络 RTT提交时一次网络 RTT典型场景分布式任务调度、库存扣减分布式配置、最终一致性事务3. 性能模型对比——冲突概率决定胜负3.1 理论性能曲线假设 100 线程并发执行 100 万次累加冲突概率悲观锁synchronized乐观锁AtomicLong乐观锁LongAdder0%2.5s1.2s1.5s5%2.8s1.5s1.6s20%4.0s3.5s2.0s50%8.0s12.0s自旋风暴3.0s80%15.0s60.0s活锁5.0s99%30.0s串行化不可用8.0s关键结论低冲突 20%乐观锁性能碾压悲观锁无上下文切换高冲突 50%悲观锁更稳定线程阻塞释放 CPU乐观锁自旋导致 CPU 打满极端冲突 90%两者都退化需队列化单线程串行处理。3.2 延迟分布对比百分位悲观锁P99乐观锁P99说明P502ms0.1μs乐观锁无阻塞延迟极低P905ms0.5μs悲观锁受线程调度影响P9950ms10ms乐观锁高冲突时重试累积P99.9200ms100ms悲观锁锁等待超时风险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也是悲观锁“乐观锁只能用于数据库”❌ JavaAtomic类、StampedLock 都是乐观锁实现“悲观锁一定保证一致性”❌ 未正确使用事务隔离级别或锁粒度仍可能出现幻读、不可重复读“高并发必须用乐观锁”❌ 秒杀等高冲突场景乐观锁重试风暴会导致系统崩溃“乐观锁无死锁”✅ 正确但可能有活锁无限重试和饥饿某些线程一直失败8. 生产环境避坑指南8.1 避免一刀切选型// ❌ 错误所有场景都用 synchronizedpublicclassBadDesign{privateMapString,ConfigconfigsnewHashMap();publicsynchronizedConfiggetConfig(Stringkey){returnconfigs.get(key);// 读操作也加锁}}// ✅ 正确读用乐观写用悲观publicclassGoodDesign{privatevolatileMapString,ConfigconfigsnewHashMap();publicConfiggetConfig(Stringkey){returnconfigs.get(key);// 读无锁volatile 保证可见性}publicsynchronizedvoidupdateConfig(Stringkey,Configconfig){MapString,ConfignewConfigsnewHashMap(configs);newConfigs.put(key,config);configsnewConfigs;// 写CopyOnWrite 思想}}8.2 监控冲突率动态调整策略// 埋点监控乐观锁冲突率CounterconflictCountermeterRegistry.counter(optimistic.lock.conflict);publicbooleanupdateWithVersion(Productproduct){introwsproductDao.update(product);if(rows0){conflictCounter.increment();// 冲突率 20% 时告警提示改用悲观锁returnfalse;}returntrue;}8.3 数据库乐观锁必须配合索引-- ❌ 错误version 无索引全表扫描UPDATEproductSETstockstock-1,versionversion1WHEREid1ANDversion5;-- id 是主键OK-- ❌ 危险按非索引字段更新UPDATEproductSETstockstock-1,versionversion1WHEREskuABC123ANDversion5;-- sku 无索引 → 全表扫描 表锁8.4 分布式锁必须设置超时// ❌ 错误无超时死锁后永久阻塞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 资源整体吞吐量更高。追问 3“数据库中悲观锁和乐观锁怎么选”高分回答数据库层面的选型同样取决于冲突概率和一致性要求读多写少 5% 冲突乐观锁版本号。例如商品详情页读:写 1000:1加FOR UPDATE会阻塞大量读线程读写均衡5%~30%混合策略。读走主从复制无锁写走悲观锁FOR UPDATE或乐观锁 重试写多读少 30%悲观锁。例如库存扣减冲突率高乐观锁重试风暴会导致数据库 CPU 打满强一致性金融转账悲观锁 Serializable 隔离级别或分布式事务Seata XA。关键细节乐观锁的UPDATE ... WHERE version ?必须确保 WHERE 条件走索引否则退化为全表扫描效果等同于表锁。追问 4“乐观锁的 ABA 问题悲观锁有吗”高分回答悲观锁没有 ABA 问题因为悲观锁通过物理阻塞确保操作期间没有其他线程修改数据。乐观锁的 ABA 问题分两种实现CAS 路径AtomicReference存在 ABA因为 CAS 只比较值不比较修改历史。解决用AtomicStampedReference版本号版本号路径数据库乐观锁的version字段递增天然解决 ABA值回退但版本号不同。所以 ABA 是 CAS 特有的问题版本号乐观锁和悲观锁都不存在。追问 5“分布式环境下悲观锁和乐观锁怎么选”高分回答分布式环境下两者的实现和权衡都发生了变化悲观锁的分布式化单机synchronized失效需引入 Redis RedLock、ZooKeeper 分布式锁代价网络 RTT~1-5ms、时钟漂移风险RedLock、脑裂风险适用必须强互斥的场景如分布式任务调度、全局 ID 生成。乐观锁的分布式化数据库版本号天然支持分布式无中心协调代价冲突检测在提交时网络往返后才发现冲突重试成本更高适用最终一致性场景如配置更新、缓存同步。现代趋势分布式场景下两者都在向’无锁化’演进——Saga 模式最终一致性、CRDT无锁数据结构、MVCC多版本并发控制正在替代传统的锁方案。追问 6“如果让你设计一个秒杀系统你会怎么选锁”高分回答秒杀系统是’极端高冲突’场景万人抢 100 件商品冲突率 99.99%传统锁方案都不适用需要分层解耦流量层Nginx Lua 限流99% 请求直接拒绝只剩 1% 进入后端缓存层Redisdecr预减库存原子操作无锁库存为 0 直接返回’已售罄’消息队列通过 MQ 异步下单队列化串行处理彻底消除并发冲突数据库层最终一致性写入用乐观锁版本号兜底冲突率已极低降级策略Redis 降级为本地缓存MQ 降级为直接写库 悲观锁最后防线。核心思想不是’选哪种锁’而是’让冲突不要发生’。通过限流、缓存、队列化三层过滤将数据库层面的冲突率从 99.99% 降到 1%此时乐观锁轻松应对。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 正在替代传统锁方案。最后记住最好的锁是不用锁——通过架构设计让冲突不要发生比选哪种锁更重要。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~