
先说结论悲观锁和乐观锁是并发编程里绕不开的两个基础概念也是Java面试中出现频率非常高的一题。面试官问这个问题通常不是只等你背出“悲观锁默认冲突乐观锁默认不冲突”这两句话而是要看你能不能落到实现层面能不能说清加锁粒度、性能开销、死锁风险、ABA问题以及在不同业务场景里怎么选。这篇文章就按面试答题的逻辑拆一遍同时把代码实现、判断标准、踩坑经验一起写清楚。如果你正在准备Java面试或者做业务系统时被并发扣减、重复提交、超卖这类问题卡住这篇文章可以直接当一份复习材料用。如果你已经写过很多同步代码也可以重点看后面几部分锁升级、CAS底层、ABA处理、自旋和阻塞的取舍。这里不会只讲理论每一段都会落到实际代码和真实场景里。1. 悲观锁和乐观锁的本质区别先建立一个判断框架1.1 悲观锁默认认为并发冲突很严重悲观锁的思路是我这次操作很可能和别人冲突所以先把我需要的资源锁住别人想碰就得等我用完。这个“锁住”的动作在Java里有两种典型实现一种是synchronized关键字一种是ReentrantLock在数据库层面则是SELECT ... FOR UPDATE。悲观锁的好处是强一致性很好理解拿到锁的人做修改其他线程要么排队要么直接失败数据不会被中间状态污染。坏处是代价高加锁、阻塞、唤醒都需要操作系统和JVM参与高并发下容易出现线程排队耗时、锁竞争加剧、甚至死锁的问题。你去看大部分教科书都会说“悲观锁适合写多读少、并发冲突严重的场景”。这句话方向没错但不够具体。真正落地时你还要回答一个问题冲突率到底多高算高没有统一标准一般我会按业务指标判断比如秒杀场景库存只有10件请求量成千上万冲突率极高这时候用悲观锁更稳妥。再比如普通后台系统里同一张订单记录同时被改的概率很低用乐观锁就够了。1.2 乐观锁默认认为并发冲突不严重乐观锁的思路反过来我读取数据的时候不加锁到了更新的时候才检查数据有没有被别人改过。如果在读取到更新这段期间数据变了这次更新就失败或者重试。这个“有没有被改过”的检查常见做法是版本号机制也有用CAS比较并交换在无锁情况下实现。乐观锁不需要线程阻塞所以并发高时没有锁竞争和上下文切换的开销。但代价是需要重试机制。如果冲突频繁CAS会反复失败CPU空转性能反而比悲观锁更差。所以乐观锁适合的场景是读多写少、冲突率低、响应要求高。典型例子是博客点赞数、浏览数、购物车商品信息更新、配置表更新等。这类操作如果加悲观锁大部分时间都是白白排队用乐观锁更合适。1.3 一句话记忆方式和面试切入角度我建议你形成这样一句属于自己的理解悲观锁是“先拿锁再操作”乐观锁是“先操作再验证”。面试时不要只说定义一定要补一句判断标准“选择哪种锁核心看冲突概率和重试成本。冲突率高、重试代价大选悲观锁冲突率低、能接受失败重试选乐观锁。”这句话一出来面试官就知道你不是背题而是真的在场景里做过取舍。2. Java中悲观锁的三种实现方式别只答synchronized2.1 synchronized最常用的JVM内置锁synchronized是最基础的悲观锁实现用法简单作用于方法或代码块底层由JVM的监视器锁Monitor支持。它最容易被忽略的一点是可重入也就是说同一个线程可以多次获取同一把锁不会自己把自己锁死。public class StockService { private int stock 100; public synchronized void deduct() { if (stock 0) { stock--; System.out.println(扣减成功剩余库存 stock); } else { System.out.println(库存不足); } } }这里有一个细节在JDK 1.6之前synchronized是重量级锁性能表现一般。但是从JDK 1.6开始引入锁升级机制也就是无锁、偏向锁、轻量级锁、重量级锁这样一条升级路径。在低竞争场景下synchronized的额外开销很小。这也是为什么现在很多项目里简单的并发控制直接用synchronized就够了不需要一上来就上ReentrantLock。面试如果被问到“锁升级过程是怎样的”你可以简洁回答JVM先判断对象头里的锁标记如果只有一个线程访问就偏向有竞争就升级成轻量级锁用CAS自旋尝试获取自旋超过阈值或竞争激烈再膨胀成重量级锁进入阻塞等待。2.2 ReentrantLock更灵活的JDK显式锁ReentrantLock是java.util.concurrent.locks包下的显式锁需要手动加锁和解锁。相比synchronized它多了几个能力可中断获取锁、可超时获取锁、公平锁和非公平锁可配置还能配合Condition做更细粒度的等待通知。public class StockServiceWithLock { private final ReentrantLock lock new ReentrantLock(); private int stock 100; public void deduct() { lock.lock(); try { if (stock 0) { stock--; System.out.println(扣减成功剩余库存 stock); } else { System.out.println(库存不足); } } finally { lock.unlock(); } } }用ReentrantLock最容易犯的错是忘记在finally里解锁。一旦中间抛异常锁就永远不会释放线程直接卡死。这是非常经典的生产事故。所以我的习惯是写加锁代码时先写try-finally结构再往里面填业务逻辑不要让解锁逻辑漏掉。2.3 数据库悲观锁SELECT ... FOR UPDATEJava层面的锁只管单机应用进程内。如果你部署多台机器多个JVM进程之间需要同步Java内置锁就失效了只能依赖分布式锁或者数据库锁。数据库悲观锁是比较古老但仍然是面试常考点。-- 事务内先锁住这条记录 SELECT * FROM stock WHERE id 1 FOR UPDATE; -- 业务处理完成后再更新 UPDATE stock SET stock stock - 1 WHERE id 1; COMMIT;这里要特别注意FOR UPDATE必须在事务里生效并且要放在事务提交或回滚后才释放。如果忘记开启事务或者连接池把连接还回去时事务没有提交锁的行为就会变得不可控。另外要确认查询走了索引如果锁的是全表扫描的记录代价会非常大。3. Java中乐观锁的实现方式核心是CAS和版本号3.1 CAS的原子操作原理CAS全称是Compare And Swap比较并交换。它是一条CPU原子指令做的事情是先比较内存里的值是不是等于我预期的值如果等于就更新成新值整个操作不可分割。Java里java.util.concurrent.atomic包下的大量原子类底层都是通过Unsafe类调用CAS完成的。public class AtomicStockService { private AtomicInteger stock new AtomicInteger(100); public void deduct() { while (true) { int current stock.get(); if (current 0) { System.out.println(库存不足); return; } boolean success stock.compareAndSet(current, current - 1); if (success) { System.out.println(扣减成功剩余库存 (current - 1)); return; } // CAS失败说明有其他线程改过了继续循环 } } }这段代码就是典型的自旋CAS写法。compareAndSet传入两个参数第一个是期望值第二个是新值。只有当内存当前值等于期望值时才会更新。如果失败就重新读取最新值再试。这里要理解一个关键点CAS不是没有锁而是把锁粒度缩小到了CPU指令级别我们感知不到阻塞而已。它解决的是“单个变量”的原子更新问题。如果业务逻辑涉及多个变量的联合修改CAS就不够用了要么用锁要么用AtomicReference包装对象要么借助数据库事务。3.2 版本号机制和ABA问题CAS虽然高效但存在一个经典问题ABA问题。什么意思呢就是线程A读取变量值时是1线程B把值改成2又改回1线程A再次CAS时发现期望值还是1就认为数据没被别人动过于是继续操作。从结果看变量值确实没变但中间状态已经被别人改过某些业务场景下这是不安全的。解决ABA问题的办法是加上版本号。Java里可以用AtomicStampedReference它保存一个对象引用和一个版本戳每次修改时版本戳也变化CAS时同时比较引用和版本戳。业务系统里更常见的做法是数据库表里加version字段更新时把版本号作为条件。UPDATE stock SET stock stock - 1, version version 1 WHERE id 1 AND version #{version};如果更新返回的影响行数是1说明版本没变更新成功。如果影响行数是0说明版本已经被其他事务改过了当前这次更新失败需要业务方重新读取数据再试一次。3.3 Atomic包的实际用法和适用边界Java的AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference是日常开发里最常用的原子类。它们适用于计数器、开关状态、单变量统计、ID生成等场景代码比synchronized更轻量。但要注意它们的边界只能保证单变量原子性不能覆盖多变量事务。自旋重试在高冲突下会大量消耗CPU。不能自动处理ABA问题需要时得用AtomicStampedReference。不能替代数据库事务因为数据库操作是跨网络、跨连接、跨进程的。如果你的业务是多步操作比如“先检查库存再扣减”“先查询用户余额再写入流水”这类复合操作不能靠单个CAS完成至少需要锁或者事务。4. 一个库存扣减场景的完整对比性能差距怎么看4.1 悲观锁版本完整代码为了更直观我这里用一个库存扣减场景把两种方式都写出来。先看悲观锁版本使用ReentrantLock保证单机多线程安全。import java.util.concurrent.locks.ReentrantLock; public class PessimisticStockService { private final ReentrantLock lock new ReentrantLock(); private int stock; public PessimisticStockService(int stock) { this.stock stock; } public boolean deduct(int count) { lock.lock(); try { if (stock count) { stock - count; return true; } return false; } finally { lock.unlock(); } } public int getStock() { return stock; } }这段代码的优点是逻辑简单库存检查和扣减整个过程被锁保护不会出现超卖。缺点是所有线程都排队执行即使库存充足也只能一个接一个扣减吞吐量受锁竞争影响。4.2 乐观锁版本完整代码再看乐观锁版本使用AtomicInteger模拟CAS同时加入重试逻辑。import java.util.concurrent.atomic.AtomicInteger; public class OptimisticStockService { private final AtomicInteger stock; public OptimisticStockService(int stock) { this.stock new AtomicInteger(stock); } public boolean deduct(int count) { while (true) { int current stock.get(); if (current count) { return false; } boolean success stock.compareAndSet(current, current - count); if (success) { return true; } // 失败说明有竞争重新读取再试 } } public int getStock() { return stock.get(); } }乐观锁版本理论上并发能力更强因为不会阻塞线程。但如果大量线程同时把current都读成同一个值只有一个线程CAS成功其他线程全部重试。请求量越大重试越频繁CPU占用越高。这就是乐观锁的典型代价。4.3 怎么判断当前环境选哪种我用简单压测判断时主要看三个指标吞吐量、成功请求数和CPU占用。可以按下面的思路做一轮对比测试准备100个库存。用1000个线程同时并发扣减。悲观锁记录总耗时和吞吐量。乐观锁记录总耗时、吞吐量和CAS失败次数。再把库存变小比如10个请求量变大比如10000个重跑一轮。通常结果是这样的在低冲突场景下乐观锁吞吐量更高在高冲突场景下悲观锁虽然排队时间长但CPU占用更稳定不会出现大量无意义自旋。不同机器、不同JVM参数下数值会有差异我建议你用自己的环境实际测一轮不要只信网上结论。5. 面试官追问清单这样回答才能拉开差距5.1 先用“三步法”答好区别面试时被问“悲观锁和乐观锁的区别”我建议你用三步结构回答核心思路对比悲观锁默认冲突严重先加锁乐观锁默认冲突少更新时检查。实现方式对比悲观锁在Java里是synchronized和ReentrantLock数据库是FOR UPDATE乐观锁在Java里是CAS原子类数据库是版本号机制。适用场景和代价悲观锁强一致但阻塞成本高乐观锁并发能力强但需要重试冲突高时反而更差。这里最忌讳只答一条定义。面试官一天面试很多人知道定义的不少能把场景和代价说清楚的人不多。5.2 高频追问和回答思路下面几个追问基本是必考的提前准备好。第一个synchronized和ReentrantLock选哪个从功能看ReentrantLock支持可中断、可超时、公平锁使用更灵活。从性能看现在两者差距不大synchronized有锁升级机制低竞争场景下很轻量。我一般推荐简单场景用synchronized代码少不容易出错需要超时中断、公平调度、多条件等待时用ReentrantLock。如果能用StampedLock的部分场景也可以提但要说明它是乐观读模式不是锁的替代品。第二个CAS底层是怎么实现的CAS不是靠Java就能完成的它最终由CPU的cmpxchg指令支持Java通过Unsafe类暴露本地方法。这里可以补一句CAS只能保证一个共享变量的原子性多个共享变量需要用锁或者AtomicReference包装。第三个什么是ABA问题怎么解决按照3.2部分的描述回答即可。重点是不要只说“有ABA问题”要说出解决手段AtomicStampedReference版本戳机制或者数据库版本号字段。最好再补一句业务影响例如“如果业务不关心中间态变化ABA可以接受如果关心就必须加版本号”。第四个乐观锁失败后怎么办常见处理有三种直接返回失败让用户重试、自动重试固定次数、用数据库乐观锁更新时把影响行数作为成功判断。自动重试时要设置次数上限和间隔不能无限循环。第五个锁和事务是什么关系这是很容混淆的一个点。Java的锁管的是多线程之间数据库事务管的是数据库操作的一致性。二者不是同一个层次。在高并发业务里经常是同时使用方法内部用锁保证JVM线程安全数据库层面用事务保证SQL操作的一致性。面试时能区分这个层次会是加分项。5.3 结合当下大模型和Agent场景怎么展开现在Java面试经常会把并发编程和大模型系统扯在一起。比如问一个大模型推理服务的请求量很高多个请求同时更新用户配额、调用次数、Token消耗这时候用悲观锁还是乐观锁我的回答思路是这种场景是典型的“读多写少”用户每次请求都会读取自己的配额但真正产生扣减的次数相对有限。可以在内存里用原子类做并发扣减在数据库里用版本号保证最终一致。如果要对齐大模型网关的限流计数用AtomicLong计数自增就够了不需要重量级锁。如果涉及多个维度比如用户、模型、时间段三段维度同时扣减那要设计单独的计数服务而不是简单加一把锁。这种回答的好处是让面试官看到你不仅能答八股还能把知识点映射到实际系统架构上。6. 生产环境选型与排查经验这些坑真的会遇到6.1 选型不只看并发量还要看冲突率和响应要求很多开发者有一个误区高并发就一定用乐观锁。实际上高并发和高冲突不是一回事。举个例子一个读多写少的首页缓存更新场景100万次访问里可能只有1次更新冲突率极低乐观锁非常合适。但秒杀场景里1万个人抢100个库存冲突率接近100%CAS会疯狂失败重试这时候还不如用分布式锁或者数据库悲观锁。所以我建议你选型时按这个顺序判断数据竞争是否发生在同一个JVM内是可以用synchronized、ReentrantLock、CAS否需要分布式锁或数据库锁。冲突率高不高高优先悲观锁低优先乐观锁。失败成本高不高扣钱、发货、下单这类高成本操作宁可排队不要反复重试点赞、计数这类低成本操作用乐观锁就好。是否需要强一致性库存不能超卖需要严格校验浏览数偶尔延迟可以容忍。6.2 常见问题和排查顺序死锁现象是线程互相持有对方需要的锁互相等待任务卡住。排查顺序是先看日志里有没有线程长时间卡在某个锁上再用jstack导出线程栈查找“Found one Java-level deadlock”关键字。解决思路是保证多个锁的获取顺序一致或者尽量缩短持锁时间。自旋重试风暴现象是CPU飙升大量线程都在CAS循环里空转。排查顺序是先看业务是否发生高冲突再看CAS失败次数是否异常高。解决思路是给自旋设置次数上限超过上限后休眠或走失败逻辑或者直接换成悲观锁。ABA问题现象是数据“看起来没变”但实际被改过业务结果异常。排查顺序是确认是否用了没有版本戳的CAS操作确认业务是否依赖变量中间状态。解决思路是在AtomicStampedReference和数据库version字段之间选一个。锁漏释放现象是方法第一次执行正常第二次就卡住像是锁没解开。排查顺序是检查lock()和unlock()是否配对异常路径是否执行了finally。这个不用看日志就能猜出大概。我把常见问题列成一个排查顺序表方便你对照着使用现象可能原因优先排查常见处理线程卡住死锁、锁未释放jstack线程栈调整锁顺序、使用try-finallyCPU飙高CAS高频自旋统计CAS失败次数设置重试上限、改用悲观锁数据被覆盖缺少版本校验检查version字段、ABA场景加版本戳、AtomicStampedReference更新影响行数为0乐观锁冲突查看重试逻辑自动重试、提示用户重新操作大批请求失败锁竞争激烈压测看吞吐和错误率分片、限流、拆分锁粒度6.3 实际开发里我会怎么做落实到真实项目我的习惯是这样简单场景能用原子类就不用锁。比如计数器、限流器、状态标记直接用AtomicLong或AtomicBoolean代码短性能好。复合操作需要保证强一致时先用synchronized因为不容易出错。确需ReentrantLock的高级能力时再换显式锁而且一定写成try-finally。跨多实例的共享资源竞争不直接用Java锁而是用数据库乐观锁或者分布式锁。数据库版本号方式最轻适合短事务如果有跨进程长时间持锁的需求直接用现成组件不要在业务代码里自己写复杂的锁管理。最后还有一个经验压测很重要。不要凭想象决定用悲观锁还是乐观锁。同一套代码同一台机器用不同并发量跑一遍记录耗时、失败率和CPU占用数据会直接告诉你答案。面试时如果你能说出“我在项目里压测过低冲突时乐观锁吞吐高高冲突时悲观锁更稳”这句话比背任何理论都管用。回到开头那个问题悲观锁和乐观锁怎么实现区别是什么。现在你应该能形成完整回答了。实现上悲观锁对应synchronized、ReentrantLock、数据库FOR UPDATE乐观锁对应CAS原子类、数据库版本号机制。区别上核心是冲突假设、阻塞策略、重试成本、适用场景四个维度。面试答题时要落到代码和场景开发选型时要落到压测数据和业务指标不要停在概念层。