面试乐观锁和悲观锁:核心原理与应用场景深度解析
从底层机制到实战应用,全方位掌握并发编程中的锁策略,助你从容应对高阶技术面试。
一、 什么是乐观锁与悲观锁?
在并发编程中,锁是保证数据一致性的核心机制。对于面试官而言,考察乐观锁和悲观锁不仅仅是询问定义,更是考察候选人对并发场景的理解深度。简单来说,这两种锁代表了两种截然不同的并发控制哲学。
悲观锁 (Pessimistic Lock)
悲观锁假定最坏的情况,每次去拿数据时都认为别人会修改,所以每次在拿数据时都会上锁。在Java中,synchronized关键字和ReentrantLock都属于悲观锁的范畴。它类似于数据库中的排他锁。
核心逻辑:“我拿数据时,别人别动。”
乐观锁 (Optimistic Lock)
乐观锁假定最好的情况,每次去拿数据时都认为别人不会修改,所以不会上锁,但在更新时会判断在此期间别人是否更新过数据。如果冲突,则重试或抛出异常。在Java中,AtomicInteger使用的CAS算法就是乐观锁。
核心逻辑:“我更新时,看看有没有人改过。”
二、 核心机制与性能对比
理解两者的区别是面试的关键。我们需要从锁的实现机制、适用场景以及性能表现三个维度进行深度剖析。
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 假设前提 | 冲突概率高,需要严格互斥 | 冲突概率低,允许并发读取 |
| 实现方式 | 数据库:排他锁(X锁) Java:synchronized, Lock |
数据库:版本号机制 Java:CAS (Compare-And-Swap) |
| 性能开销 | 高(线程阻塞、唤醒开销大) | 低(无阻塞,但重试有CPU开销) |
| 适用场景 | 写操作频繁,数据竞争激烈的场景 | 读多写少,数据竞争较少的场景 |
| 死锁风险 | 存在死锁可能 | 不存在死锁(但可能无限重试) |
在面试中,如果你能进一步指出:悲观锁在数据库层面通常通过InnoDB引擎的行锁实现,而乐观锁在数据库中通常通过version字段或CAS机制实现,这将是一个巨大的加分项。
三、 Java中的乐观锁实现:CAS详解
Java并发包(java.util.concurrent)是面试的重灾区。面试官常问:“请解释一下CAS的工作原理?”
3.1 CAS (Compare-And-Swap)
CAS是一种无锁算法,包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作,并返回失败。
// 伪代码演示CAS逻辑
if (MemoryLocation == ExpectedValue) {
MemoryLocation = NewValue;
return true;
} else {
return false; // 失败,通常需要重试
}
3.2 ABA问题
这是乐观锁实现中经典的陷阱。如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它没被改变过吗?
情况可能是:V先变成了B,又变回了A。虽然值没变,但中间的过程发生了,这可能导致逻辑错误。
解决方案:使用AtomicStampedReference,它不仅比较值,还比较版本号(时间戳)。
悲观锁示例:Synchronized
synchronized是Java内置的悲观锁实现。它依赖于JVM层面的Monitor对象,属于重量级锁(在JDK 1.6优化后有偏向锁、轻量级锁的概念)。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
特点:线程阻塞,上下文切换成本高,但代码简洁。
悲观锁示例:ReentrantLock
基于AQS(AbstractQueuedSynchronizer)实现的显式锁。它提供了比synchronized更灵活的锁机制,如可中断锁、公平锁等。
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
特点:需要手动释放锁,防止死锁,支持Condition。
乐观锁示例:AtomicInteger
利用CAS指令实现无锁并发。底层调用Unsafe类的compareAndSwapInt方法。
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();
特点:无阻塞,但在高竞争下会导致大量CAS失败和CPU自旋,性能下降。
四、 数据库中的乐观锁与悲观锁
在数据库领域,锁的概念更加具体。MySQL InnoDB引擎是面试中常讨论的对象。
4.1 悲观锁在数据库中的实现
通过SELECT ... FOR UPDATE语句实现。当执行该语句时,InnoDB会对扫描到的行加排他锁(X锁),其他事务无法对这些行进行读写(取决于隔离级别),直到当前事务提交或回滚。
注意:如果没有命中索引,MySQL会锁住整张表,导致性能急剧下降,这是面试中常见的陷阱题。
4.2 乐观锁在数据库中的实现
通常通过version字段实现。更新时检查版本号是否一致。
-- 查询数据
SELECT id, name, version FROM product WHERE id = 1;
-- 更新数据
UPDATE product SET name = 'NewName', version = version + 1
WHERE id = 1 AND version = 1;
如果version不匹配,更新影响行数为0,应用层需处理重试逻辑。
4.3 MVCC (多版本并发控制)
MySQL InnoDB通过MVCC实现了非阻塞读,这在一定程度上实现了读操作的乐观锁。通过Undo Log保存数据的历史版本,Read View决定当前事务能看到哪个版本的数据,从而避免了加锁。
五、 难点攻克:ABA问题详解
ABA问题是乐观锁最著名的缺陷。虽然值没变,但状态可能已经经历了“变化-恢复”的过程,这在金融交易或状态机跳转中是危险的。
用户A余额100元。线程1准备将余额改为150元(+50)。此时线程2介入,将余额改为150元,又改为100元(-50)。
线程1执行CAS时,发现当前值仍是100(与预期一致),于是执行+50操作,余额变为150元。但实际上,线程2的操作被忽略了,导致逻辑错误。
使用版本号机制。每次修改数据时,版本号+1。这样线程2的操作会使版本号从1变为3,线程1的CAS检查版本号时会发现不一致,从而失败。
六、 网友们还关心:高频面试问答
根据搜索引擎数据和社区讨论,以下是求职者最关心的周边知识点:
Q1: 什么时候用乐观锁,什么时候用悲观锁?
A: 读多写少用乐观锁,写多读少用悲观锁。如果冲突概率极低,乐观锁性能好;如果冲突概率高,悲观锁避免大量重试开销,性能更优。
Q2: CAS的缺点是什么?
A: 1. 循环时间长开销大(自旋);2. 只能保证一个共享变量的原子操作;3. ABA问题。
Q3: synchronized和Lock有什么区别?
A: 1. synchronized是关键字,Lock是接口;2. synchronized自动释放锁,Lock需手动释放;3. Lock支持公平锁,synchronized非公平;4. Lock支持中断响应,synchronized不支持。
Q4: 数据库死锁是如何产生的?
A: 两个或多个事务在同一资源相互占有,互相请求对方的资源,导致无限等待。解决方案:按固定顺序访问资源、设置超时时间、一次性申请所有资源。
六、 拓展知识:并发编程的其他锁机制
除了乐观和悲观锁,现代并发编程中还有以下概念值得关注:
- ⚡ 读写锁 (ReadWriteLock):允许多个线程同时读,但只允许一个线程写。适合读多写少的场景。
- ⚙️ 分段锁 (Segmented Lock):如ConcurrentHashMap在JDK 1.7中使用,将数据分段,每段加一把锁,提高并发度。
- ⚡ 偏向锁与轻量级锁:JVM对synchronized的优化,在无竞争情况下减少锁获取开销。
七、 总结
掌握乐观锁和悲观锁不仅是记忆概念,更要理解其在不同场景下的权衡。在面试中,结合具体的业务场景(如秒杀扣库存、账户余额修改)来分析锁的选择,才能体现出你的实战能力。希望本篇指南能帮助你构建完整的知识体系。