Java并发编程:悲观锁与乐观锁的核心原理与实战选型 最近在准备 Java 面试的同学十有八九会被问到并发编程。而并发编程里悲观锁和乐观锁又是一道绕不开的经典题目。面试官通常会从“什么是悲观锁、什么是乐观锁”切入然后逐步追问“Java 里怎么实现”“底层原理是什么”“实际项目中怎么选”。如果只是背出概念往往会被继续追问到 CAS、synchronized 原理、ABA 问题、数据库乐观锁写法这些更深层的细节。本文将围绕这道高频面试题展开先讲清楚悲观锁和乐观锁的核心思想再分别给出 Java 层面的实现方式包含完整的可运行代码示例最后总结两者的区别、适用场景、常见坑点以及一套能直接用在项目里的选型思路。文章内容既适合面试前突击复习也适合日常开发时作为并发编程的参考笔记。1. 悲观锁和乐观锁到底在解决什么问题在正式讲实现之前我们先要理解一个核心问题为什么需要锁在单线程环境下代码是一行一行顺序执行的不存在资源竞争。但一旦进入多线程环境多个线程同时读写同一个共享变量就会出现数据不一致的问题。比如经典的i操作看起来是一条语句但在 CPU 层面其实是“读取 → 计算 → 写回”三步。两个线程同时执行i最终结果可能只加了一次这就是线程安全问题。为了解决这个问题最直观的思路就是让多个线程在访问共享资源时“排队”保证同一时刻只有一个线程能操作。但排队的方式又分为两种截然不同的思路这就是悲观锁和乐观锁的由来。悲观锁默认认为并发冲突一定发生。所以线程在操作共享资源之前先把资源锁住其他线程只能等待当前线程释放锁后再去竞争。相当于“先上车再买票”保证整个过程是独占的。乐观锁默认认为并发冲突很少发生。所以线程不主动加锁而是直接操作资源但在写入的时候检查一下“在我操作的过程中有没有别人改过这个数据”。如果没人改过就提交成功如果被人改过了就放弃或者重试。相当于“先上车到站再验票”用最终校验代替全程独占。很多初学者会把这两者理解成“两种 API”其实它们是两种设计思想。Java 里的synchronized、ReentrantLock是悲观锁的典型实现而AtomicInteger、数据库的版本号机制、CAS 算法则是乐观锁思想的典型实现。2. 悲观锁在 Java 中的实现2.1 synchronized 关键字synchronized是 Java 提供的最基础的线程同步关键字也是面试中最常被问到的悲观锁实现。它可以直接修饰方法也可以修饰代码块。我们先看一个最简单的示例。模拟多个线程同时对一个共享变量执行自增操作不加锁的时候结果会小于预期值。// 文件路径src/main/java/com/example/lock/NoLockDemo.java public class NoLockDemo { private static int count 0; public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { count; } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count); // 预期 100000实际运行大概率小于 100000 } }运行结果通常不是 100000而是某个小于 100000 的数这就是线程安全问题。现在用synchronized改造// 文件路径src/main/java/com/example/lock/SynchronizedDemo.java public class SynchronizedDemo { private static int count 0; public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { increment(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count); // 输出 100000 } private static synchronized void increment() { count; } }synchronized加在静态方法上锁的是当前类的 Class 对象所有线程在进入increment方法前都要先获取这把锁。获取不到锁的线程会进入阻塞状态直到持锁线程执行完毕释放锁。synchronized也可以修饰代码块这样锁粒度更小性能更好// 文件路径src/main/java/com/example/lock/SynchronizedBlockDemo.java public class SynchronizedBlockDemo { private static int count 0; // 专门用于加锁的对象 private static final Object LOCK new Object(); public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { synchronized (LOCK) { count; } } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count); } }2.2 ReentrantLock 可重入锁synchronized是隐式锁使用简单但在功能上不够灵活。ReentrantLock是java.util.concurrent.locks包下提供的显式锁需要手动加锁和释放锁但支持公平锁、非公平锁、可中断、可超时等高级特性。// 文件路径src/main/java/com/example/lock/ReentrantLockDemo.java import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private static int count 0; private static final ReentrantLock LOCK new ReentrantLock(); public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { LOCK.lock(); try { count; } finally { LOCK.unlock(); } } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count); } }这里有一个非常重要的细节LOCK.unlock()必须放在finally块中。如果业务代码抛出异常锁仍然能被释放否则会出现死锁。ReentrantLock支持公平锁和非公平锁。默认是非公平锁可以通过构造方法传入true启用公平锁ReentrantLock fairLock new ReentrantLock(true);公平锁会让等待时间最长的线程优先获得锁减少“线程饥饿”问题但性能稍低。非公平锁性能更高但可能出现某些线程一直抢不到锁的情况。在面试中如果被问到公平锁和非公平锁的区别可以参考这个角度回答。2.3 悲观锁的底层原理简析synchronized在 JDK 1.6 之后引入了锁升级机制不再是单纯的重型锁。锁的状态从低到高依次是无锁 → 偏向锁 → 轻量级锁 → 重量级锁。JVM 会根据竞争激烈程度自动升级。偏向锁只有一个线程访问同步块时锁会偏向该线程省去反复获取锁的开销。轻量级锁有竞争但仍不激烈时通过 CAS 自旋获取锁避免线程阻塞和唤醒的开销。重量级锁竞争非常激烈时锁会升级为重量级锁未获取到锁的线程会进入阻塞状态。ReentrantLock底层依赖于AbstractQueuedSynchronizerAQS实现。AQS 内部维护了一个volatile int state变量和一个等待队列。线程尝试获取锁时通过 CAS 修改state修改成功说明获取锁成功失败则进入等待队列排队。面试中如果被追问底层原理能说出来 synchronized 的锁升级过程和 AQS 的排队机制就已经能超过大部分候选人了。3. 乐观锁在 Java 中的实现3.1 CAS 机制乐观锁的核心是 CAS全称 Compare-And-Swap也就是“比较并交换”。CAS 操作包含三个操作数内存位置V、预期原值A、新值B。执行时先比较V中的值是否等于A如果相等就把B写入V如果不相等说明有其他线程修改过操作失败。CAS 是 CPU 层面提供的原子指令Java 通过Unsafe类来调用底层的 CAS 能力。开发中一般不直接操作Unsafe而是使用java.util.concurrent.atomic包下的原子类。3.2 AtomicInteger 原子类以AtomicInteger为例看一下乐观锁最简单的使用方式// 文件路径src/main/java/com/example/lock/AtomicDemo.java import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { private static AtomicInteger count new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { count.incrementAndGet(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count.get()); // 输出 100000 } }incrementAndGet()方法内部就是一个 CAS 自旋操作。如果 CAS 失败就重新读取最新值再次尝试直到成功为止。这个过程不需要加锁也不会阻塞线程所以在并发量不是特别大的场景下性能很好。3.3 手动实现 CAS 乐观锁理解 CAS 最好的方式是自己动手造一个简化版。下面这个示例模拟了 AtomicInteger 的 CAS 自旋过程// 文件路径src/main/java/com/example/lock/CasDemo.java import java.util.concurrent.atomic.AtomicInteger; public class CasDemo { private static AtomicInteger value new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { compareAndSwap(0, 1); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 value.get()); } /** * 模拟 CAS 自旋 * 反复读取当前值当当前值等于预期值时执行更新。 * 注意这里为了演示 CAS 思想做了简化实际原子操作由 AtomicInteger 提供。 */ private static void compareAndSwap(int expect, int update) { int current; do { current value.get(); } while (!value.compareAndSet(current, current update)); } }这里用一个do-while循环来模拟失败重试。compareAndSet(current, current update)是真正原子的 CAS 操作。如果current值在这期间被其他线程改了compareAndSet会返回false循环继续直到成功。3.4 数据库场景的乐观锁版本号机制在 Java 面试中除了问 JVM 层的乐观锁实现还经常扩展到数据库层面。数据库的乐观锁通常使用版本号字段实现。假设有一个商品表CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, stock INT NOT NULL, version INT NOT NULL DEFAULT 0 ); INSERT INTO product (name, stock, version) VALUES (手机, 100, 0);下单扣减库存时先查出商品信息包含当前版本号SELECT id, name, stock, version FROM product WHERE id 1;业务处理完毕后执行更新更新时带上版本号条件UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND version 0;UPDATE语句影响的行数为 1说明更新成功期间没有其他线程修改过。如果影响行数为 0说明版本号已过期需要重新查询并重试。这里需要注意UPDATE语句本身是加锁的在数据库层面会锁定匹配的行。但乐观锁的“乐观”体现在业务层不阻塞读取而是在更新时通过版本号校验来保证一致性。如果并发量很高更新失败率也会上升需要配合重试机制。使用乐观锁改造后的扣库存代码逻辑如下// 文件路径src/main/java/com/example/lock/OptimisticLockService.java public class OptimisticLockService { /** * 模拟数据库乐观锁更新。 * 实际项目中这里会调用 MyBatis/JPA 等 ORM 框架执行 SQL。 */ public boolean deductStock(int retryTimes) { int currentRetry 0; while (currentRetry retryTimes) { // 1. 查询当前商品信息 Product product getProductById(1L); int currentVersion product.getVersion(); // 2. 执行更新 int rows updateStockAndVersion(1L, currentVersion); if (rows 0) { return true; } // 3. 更新失败说明版本号变化重试 currentRetry; } return false; } }3.5 AtomicLong/LongAdder 与性能优化除了AtomicIntegerJava 还提供了AtomicLong、AtomicReference等原子类。在 JDK 8 之后还引入了LongAdder。在高并发场景下AtomicLong的 CAS 自旋可能会因为竞争太激烈导致大量线程自旋空转性能下降。LongAdder把单一变量拆分成多个单元每个线程操作自己的单元最后再累加减少了 CAS 冲突适合“读少写多”的统计场景。// 文件路径src/main/java/com/example/lock/LongAdderDemo.java import java.util.concurrent.atomic.LongAdder; public class LongAdderDemo { private static LongAdder count new LongAdder(); public static void main(String[] args) throws InterruptedException { int threadSize 10; Thread[] threads new Thread[threadSize]; for (int i 0; i threadSize; i) { threads[i] new Thread(() - { for (int j 0; j 10000; j) { count.increment(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println(最终结果 count.sum()); } }4. 悲观锁和乐观锁的区别对比面试中问到“悲观锁和乐观锁的区别”本质上是在考察对并发控制两种思想的理解。我们可以从锁的获取时机、性能特点、适用场景三个角度来回答。对比维度悲观锁乐观锁核心思想先锁后操作先操作后校验锁获取时机访问共享资源前必须获取锁无锁写入时校验数据是否阻塞会阻塞其他线程不阻塞通过自旋或重试实现方式synchronized、ReentrantLock、数据库 SELECT ... FOR UPDATECAS、Atomic 原子类、版本号机制并发冲突冲突多、竞争激烈时适合冲突少、读多写少时适合性能特点锁竞争激烈时可能产生大量阻塞和上下文切换冲突低时性能极高冲突高时会频繁自旋重试典型应用转账、库存扣减、订单状态变更分布式 ID 生成、计数统计、配置更新这里有一个容易踩坑的点乐观锁在冲突少时性能远高于悲观锁但一旦冲突率高CAS 自旋会持续消耗 CPU性能反而可能不如悲观锁。所以在实际项目中选择哪种锁要基于业务并发量来评估不能一概而论。5. 乐观锁的经典问题ABA 问题面试官在追问乐观锁时大概率会问 ABA 问题。这是一个必须掌握的知识点。所谓 ABA 问题指的是线程 T1 读取到的值原本是 A在 T1 执行期间线程 T2 把值从 A 改成了 B又改回了 A当 T1 执行 CAS 时发现内存中的值仍然是 A于是 CAS 成功。但实际上这个值已经被改动过两次compareAndSet的“预期值匹配”并不能发现这个变化。ABA 问题在部分场景下无所谓但在一些依赖状态变化的场景里会引发事故。比如一个用 CAS 管理链表结构的场景如果节点被其他线程修改后又恢复原状可能会导致链表结构出错。解决 ABA 问题的思路是引入版本号或时间戳Java 中对应的实现是AtomicStampedReference// 文件路径src/main/java/com/example/lock/AtomicStampedReferenceDemo.java import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceDemo { private static AtomicStampedReferenceInteger ref new AtomicStampedReference(100, 0); public static void main(String[] args) { int stamp ref.getStamp(); Integer value ref.getReference(); System.out.println(初始值 value 版本号 stamp); boolean success ref.compareAndSet(100, 200, stamp, stamp 1); System.out.println(第一次更新 success); // 使用旧版本号再次更新会失败因为版本号不匹配 boolean fail ref.compareAndSet(200, 300, stamp, stamp 1); System.out.println(使用过期版本号更新 fail); } }AtomicStampedReference在每次更新时不仅校验对象值还额外校验版本号从而规避 ABA 问题。对应到数据库场景我们用的version字段本质上就是这个思路。6. 实际场景中如何选择锁很多初学者学完概念后一到真实项目就不知道怎么选。这里给出一套可落地的选型思路。6.1 从并发冲突概率出发如果业务并发量低冲突概率小优先使用乐观锁。比如用户修改自己的个人资料不同用户之间的操作互相不干扰即使偶尔发生冲突重试一次代价也很低。如果业务并发量高冲突频繁比如秒杀场景下的库存扣减悲观锁可能更合适。因为乐观锁在冲突高时会产生大量无效重试DB 的压力反而更大。6.2 从操作耗时出发如果业务操作本身耗时较长比如一个事务里要查询大量数据再做复杂计算乐观锁在中间步骤不会持有锁其他线程可以继续读取旧数据整体并发能力会更好。但如果用悲观锁锁的持有时间会很长其他线程一直阻塞容易拖垮系统。6.3 从一致性要求出发有些业务对一致性要求极高不允许出现最终一致那就需要用悲观锁保证严格串行。比如金融转账、订单金额变更。而计数类的业务比如 PV/UV 统计、点赞数可以接受一定程度上的一致性误差适合乐观锁甚至 LongAdder 这样的无锁方案。6.4 从分布式环境出发上面的讨论仅限于单机 JVM 场景。如果在分布式环境下synchronized 和 JVM 内部的 ReentrantLock 只能锁住当前应用实例无法跨服务协调。此时需要引入分布式锁比如基于 Redis 的SETNX、Redisson 的RLock或者基于 ZooKeeper 的临时顺序节点。分布式锁本质上是悲观锁思想的延伸。而分布式场景下的乐观锁通常还是用数据库版本号或者把数据写入内部存储时通过版本校验来做。7. 面试高频追问常见问题与排查思路这里整理一份面试中围绕悲观锁和乐观锁的高频追问清单供大家自测。7.1 synchronized 和 ReentrantLock 有什么区别这是悲观锁实现里最常考的对比题。对比维度synchronizedReentrantLock锁的获取释放隐式由 JVM 自动管理显式需要手动 unlock是否可中断不支持支持 lockInterruptibly()是否支持超时不支持支持 tryLock(timeout)公平锁不支持支持构造函数传入 fair条件变量只有 wait/notify支持多个 Condition底层实现锁升级机制JVM 管理AQS 框架纯 Java 实现7.2 CAS 自旋会有什么问题三个核心问题ABA 问题前面说过用AtomicStampedReference解决。自旋消耗 CPU高并发下大量线程空转可以用LongAdder分散竞争或者退化为阻塞锁。只能保证单个变量的原子性如果要原子更新多个变量需要用AtomicReference组合成对象或者直接用锁。7.3 数据库乐观锁更新失败怎么办面试中的标准回答是“增加重试机制”。但真实项目里要做更细致的设计限制最大重试次数比如 3 次避免无线循环。每次重试前重新查询最新数据而不是用旧数据。记录失败日志用于监控和排查。如果失败率很高考虑在业务层引入分布式锁或消息队列削峰。7.4 锁粒度怎么选择锁不是越细越好。锁粒度太粗比如给整个方法加锁并发能力下降锁粒度太细比如每个小操作都单独加锁可能会出现多个锁嵌套导致死锁风险。建议是锁定“共享资源操作”的最小范围并且不在持锁期间做耗时的 IO 调用。8. 最佳实践与工程建议最后结合实际项目经验给出几条最实用的建议。能用并发工具类解决问题就不用手写锁。JDK 的java.util.concurrent包已经提供了大量经过优化的并发工具比如ConcurrentHashMap、BlockingQueue、CountDownLatch、Semaphore。新手阶段可以先熟练使用这些工具再深入研究底层原理。尽可能减小锁的范围。不要给整个方法加锁而是锁住真正需要保护的那几行代码。锁的范围越小其他线程可以并发执行的区间越大系统吞吐量越高。优先考虑读写分离的锁策略。如果共享数据读操作远多于写操作使用ReadWriteLockReentrantReadWriteLock或StampedLock可以让多个读线程同时获取锁只在写线程进入时互斥。多线程访问共享变量时养成使用原子类或并发工具的习惯。不要依赖volatile解决所有并发问题。volatile只能保证可见性不能保证复合操作的原子性。count这样的操作即使变量被声明为 volatile仍然是线程不安全的。在数据库层面做乐观锁更新时尽量把 SQL 写成单条语句。比如扣减库存UPDATE product SET stock stock - 1, version version 1 WHERE id 1 AND stock 0;这样既通过stock 0避免了超卖又通过版本号保证并发一致性。涉及批量更新时务必先在测试环境验证 SQL 的执行计划和影响行数。最后面试中回答这道题时不要只背概念。你在回答中如果能主动提到 synchronized 的锁升级、CAS 的 ABA 问题、数据库版本号机制以及分布式锁的延伸就能把这道基础题答成区分度很高的题。真正打动面试官的往往不是你背诵了多少知识点而是你能不能把知识点串起来结合业务场景给出合理的选型判断。

相关新闻

最新新闻

微信小程序酒类商城模板源码解析:从解压到上线避坑指南

微信小程序酒类商城模板源码解析:从解压到上线避坑指南

简介:这是一套专为酒类电商场景设计的微信小程序源码模板,面向前端开发者及小程序初学者,解决从零搭建专业酒类商城效率低、功能模块复用难的问题。资源共190个文件,涵盖38个JS逻辑文件(含app.js、request.js、orderWi…

2026/8/31 21:25:55
搜狗2020校招后端笔试复盘:编程题与系统设计全解析

搜狗2020校招后端笔试复盘:编程题与系统设计全解析

搜狗2020校招后端笔试(第一场)是我秋招季里印象很深的一场。搜狗这个公司,做搜索起家,后来输入法、AI硬件都有布局,所以它的后端笔试天然带着一股"实用主义"的味道——不搞偏题怪题,但每一道题都…

2026/8/31 21:25:55
STC单片机驱动电子墨水屏:硬件SPI配置与调试实录

STC单片机驱动电子墨水屏:硬件SPI配置与调试实录

简介:本资源是一套基于STC单片机驱动电子墨水屏的嵌入式显示程序,面向嵌入式初学者、单片机课程设计者及低功耗显示应用开发者,解决硬件SPI接口驱动电子纸屏的核心技术难点。项目采用纯C语言实现,包含完整初始化、图形绘制&#x…

2026/8/31 21:25:55
我的创作纪念日|从踩坑笔记到技术沉淀,我的CSDN创作之路

我的创作纪念日|从踩坑笔记到技术沉淀,我的CSDN创作之路

深耕嵌入式与蓝牙研发领域多年,无数个日夜沉浸在Linux调试、BSP驱动开发与蓝牙协议栈落地实战中,我深知一线技术开发的不易。项目迭代中总会遇到各类小众疑难问题,官方文档晦涩片面,网络资料零散陈旧,很多坑只能自己一…

2026/8/31 21:25:54
缓存穿透、击穿和雪崩:别把三种问题混成一件事

缓存穿透、击穿和雪崩:别把三种问题混成一件事

缓存穿透、击穿和雪崩:别把三种问题混成一件事缓存能够减少数据库读取,也能让高频页面更快返回。但缓存不是简单加在数据库前面就结束了。请求查不到数据、热点刚好失效、大量键同时过期、缓存服务本身异常,都会让流量重新落到后端。若团队把…

2026/8/31 21:25:54
可视化GUI自动操作实战:从批量录入到稳定性优化

可视化GUI自动操作实战:从批量录入到稳定性优化

简介:Automation Operation 2.60 是一款面向办公人员、测试工程师及低代码需求用户的可视化自动化操作工具,无需编程基础即可通过拖拽式GUI快速构建鼠标模拟、键盘输入、图像识别、OCR文字提取、浏览器控制等复杂流程,有效解决重复性操作、数…

2026/8/31 21:20:54