引言
朋友们,今天我们来聊聊 Java 面试中一个经常被问到的高频问题——乐观锁和悲观锁!这不仅是社招面试的重点,也是工作中优化并发性能的必备知识。
作为一个经历过 N 场面试、踩过无数坑、在面试官手里九死一生的“战损程序员”,我可以很负责任地告诉你:
面试官喜欢问这个问题!
所以,今天我就用故事+代码+实战的方式,把这个问题讲透!看完这篇文章,你的面试表现必定技高一筹!
故事背景:抢票大战
假设你有一个抢票系统,用户在高峰期疯狂点击“抢票”按钮,每张票都炙手可热。
- 乐观锁 的思路是:“我觉得你不会抢走这张票,我先买着,等我支付时再检查票是否被抢光。”
- 悲观锁 的思路是:“谁也别抢,我先锁住这张票,等我支付完再放开。”
我们用更形象的比喻来理解:
图片
那么,两者的具体实现方式又是什么呢?继续往下看!
悲观锁的实现方式
1. Synchronized & ReentrantLock
悲观锁的核心思想是独占式访问,即当前线程访问资源时,会阻塞其他线程的访问。最典型的实现方式是 Synchronized 和 ReentrantLock。
(1)Synchronized
图片
特点:
- JVM 内置锁,使用方便。
- 适用于简单场景,如同步方法或同步代码块。
(2)ReentrantLock
图片
特点:
- 显示加锁、解锁,比 Synchronized 更灵活。
- 可实现公平锁(避免某些线程一直得不到锁)。
2. 数据库悲观锁(for update)
如果你的抢票逻辑是基于数据库的,我们可以使用 悲观锁 来防止超卖:
图片
特点:
- 锁住行数据,其他事务无法修改。
- 适用于高并发场景,但可能导致锁竞争,影响性能。
乐观锁的实现方式
1. CAS(Compare and Swap)
CAS 是乐观锁的核心机制,它的思路是先读取数据,更新时检查数据是否被改动,如果没有变化就更新,否则重试。
(1)Java Atomic 类
图片
特点:
- 无锁机制,高并发下性能更好。
- 适用于计数、累加等轻量级操作。
2. 数据库乐观锁(版本号机制)
在数据库中,我们可以用版本号来实现乐观锁。
(1)数据库表设计
图片
(2)更新逻辑
图片
特点:
- 只有当 version 没变,才会成功更新,否则说明数据被别人改了,需要重试。
(3)Java 代码实现
图片
乐观锁 vs 悲观锁:如何选择?
图片
总结:
- 读多写少,优先用 乐观锁(减少阻塞)。
- 读写频繁,考虑 悲观锁(避免冲突)。
- 高并发环境,推荐 CAS 或版本号机制。
- 数据库更新,根据实际情况选择 for update(悲观锁)或 version(乐观锁)。
面试官可能的追问点
1、CAS 有哪些缺点?
- ABA 问题(解决方案:AtomicStampedReference)
- 自旋失败(高并发下可能导致 CPU 消耗大)
2、如何优化数据库悲观锁?
- 限制锁定的范围(只锁定必要字段)
- 使用合适的事务隔离级别(避免死锁)
3、在分布式环境下如何实现乐观锁?
- Redis 分布式锁(如 Redisson)
- Zookeeper 临时节点