Java 中常见的锁有哪些?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
体系化认知:面试官不仅仅是想知道你用过哪些锁,更是想知道你能不能从不同维度(锁的设计思想、实现方式、竞争策略等)把锁分类讲清楚。零散背诵和体系化理解,在面试官眼里一眼就能看出来。
-
原理深度:考察你是否理解
synchronized的锁升级机制(偏向锁 → 轻量级锁 → 重量级锁),ReentrantLock的 AQS 实现原理,以及 CAS 乐观锁的底层支持。 -
实战选型能力:不同的业务场景该用什么锁?为什么?能不能讲清楚
synchronized和ReentrantLock的选型依据。
核心答案
Java 中的锁不是孤立的概念,而是可以从 多个维度 来分类。核心分类如下:
上图展示了 Java 锁的完整分类体系。整体可以从以下几个维度来理解:
- 按设计思想:悲观锁认为冲突一定发生,先加锁再操作;乐观锁认为冲突很少,先操作再检测(CAS)
- 按竞争策略:公平锁按排队顺序获取锁;非公平锁允许插队,性能更好
- 按持有者:独享锁同一时刻只能被一个线程持有;共享锁可以被多个线程同时持有
- 按可重入性:可重入锁允许同一个线程重复获取同一把锁,避免死锁
- 按锁状态:
synchronized的锁会根据竞争情况自动升级(偏向锁 → 轻量级锁 → 重量级锁) - 按实现方式:
synchronized是 JVM 关键字级别实现;Lock接口是 JDK API 级别实现
下面逐个展开。
深度解析
一、悲观锁 vs 乐观锁
这是从 设计思想 角度分的,不是具体的某个锁实现。
悲观锁:总是假设最坏情况,认为每次操作都会有冲突,所以操作前先加锁。
// synchronized 就是典型的悲观锁
synchronized (lock) {
// 先加锁,再操作
balance -= amount;
}
// 数据库的 SELECT ... FOR UPDATE 也是悲观锁
乐观锁:总是假设最好情况,认为冲突概率很低,先操作,提交时检测是否有冲突(通常用 CAS 或版本号实现)。
// CAS(Compare And Swap)就是乐观锁的典型实现
// AtomicInteger 底层就是 CAS
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 操作:预期值为 0,新值为 1
// 数据库乐观锁:version 字段
UPDATE account SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
| 对比 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思想 | 先锁再操作,防患于未然 | 先操作再检测,冲突了再重试 |
| 实现 | synchronized、ReentrantLock | CAS、版本号 |
| 适用 | 写多读少、竞争激烈 | 读多写少、冲突概率低 |
| 性能 | 加锁有开销,但保证一致性 | 无锁开销,冲突时重试有成本 |
二、公平锁 vs 非公平锁
从 获取锁的顺序 角度分。
- 公平锁:多个线程按申请锁的顺序排队,先到先得。不会出现 "饥饿" 现象,但吞吐量低。
- 非公平锁:新来的线程直接尝试插队获取锁,失败了再去排队。吞吐量高,但可能导致排队线程长时间获取不到锁。
// ReentrantLock 支持选择公平/非公平
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
// synchronized 是非公平锁(无法设置为公平锁)
ReentrantLock 默认用非公平锁。为什么?因为公平锁每次都要检查队列里有没有排队的线程,涉及线程切换开销;而非公平锁直接尝试 CAS 抢锁,抢到了就省去了一次线程切换,性能更好。生产环境绝大部分场景用非公平锁就够了。
三、独享锁(排他锁)vs 共享锁
从 能否被多个线程同时持有 角度分。
- 独享锁(也叫排他锁、互斥锁):同一时刻只能被一个线程持有。
synchronized、ReentrantLock都是独享锁。 - 共享锁:可以被多个线程同时持有。
ReentrantReadWriteLock的读锁就是共享锁——多个线程可以同时读,但写锁是独享的。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁(共享锁)—— 多个线程可同时获取
rwLock.readLock().lock();
try {
// 读取数据
} finally {
rwLock.readLock().unlock();
}
// 写锁(独享锁)—— 只有一个线程能获取,且会阻塞读锁
rwLock.writeLock().lock();
try {
// 修改数据
} finally {
rwLock.writeLock().unlock();
}
读写锁特别适合 读多写少 的场景,比如缓存、配置中心。
四、可重入锁
可重入锁 指的是同一个线程可以重复获取同一把锁,不会被自己阻塞。
// synchronized 是可重入锁
synchronized void methodA() {
methodB(); // 在 A 持有锁的情况下调用 B,能正常获取同一把锁
}
synchronized void methodB() {
// 如果不可重入,这里就死锁了
}
// ReentrantLock 也是可重入锁(名字就告诉你了)
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.lock(); // 同一个线程可以重复加锁,内部用计数器记录
lock.unlock();
lock.unlock(); // 需要unlock相同次数
这点非常重要。如果锁不可重入,那一个 synchronized 方法调用另一个 synchronized 方法(同一把锁)就会直接死锁。Java 里 synchronized 和 ReentrantLock 都是可重入的,所以日常开发中不用担心这个问题。
五、synchronized 的锁升级(JDK 6+)
这块是面试的高频考点。JDK 6 之后,synchronized 引入了 "锁升级" 机制,根据竞争情况从低级锁逐步升级到高级锁:
上图展示了 synchronized 锁升级的完整过程。整体分四个阶段:
-
无锁:对象刚创建,还没有任何线程来访问同步代码。
-
偏向锁:第一个线程访问时,JVM 把线程 ID 写入对象头的 Mark Word 中。之后这个线程再次进入同步块,发现 Mark Word 里的线程 ID 是自己,直接进入,连 CAS 操作都不需要。本质上是 "偏心"——锁偏向第一个获取它的线程。
-
轻量级锁:当第二个线程来竞争偏向锁时,偏向锁撤销,升级为轻量级锁。此时竞争的线程通过 CAS 自旋 尝试获取锁。自旋就是不停地循环尝试,不放弃 CPU 执行时间。适合锁持有时间短的场景——线程稍微等几轮就能拿到锁。
-
重量级锁:如果自旋了一定次数还没拿到锁,说明竞争激烈,升级为重量级锁。此时拿不到锁的线程会被 挂起(阻塞),交由操作系统调度。涉及用户态到内核态的切换,开销比较大。
关键点:锁升级是 单向 的,只能从低到高升级,不能降级(不过有特例,比如在全局安全点 STW 期间可以降级)。这个设计的核心思想是 延迟重量级锁的使用——大部分情况下锁竞争并不激烈,用轻量级的方案就够了。
六、synchronized vs ReentrantLock
这是面试中被问频率最高的一组对比,放个表格一目了然:
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 关键字,底层用 Monitor | JDK API,基于 AQS |
| 加锁方式 | 自动加锁释放 | 手动 lock() / unlock() |
| 是否可中断 | 不可中断 | 可中断(lockInterruptibly()) |
| 公平性 | 只支持非公平 | 支持公平和非公平(构造参数) |
| 条件变量 | 只有一个等待队列(wait/notify) | 支持多个条件队列(Condition) |
| 锁升级 | 支持偏向锁 → 轻量级锁 → 重量级锁 | 不支持,直接 CAS + AQS |
| 适用场景 | 简单同步、代码块保护 | 需要高级功能(超时、中断、公平) |
怎么选?简单的同步场景用 synchronized,代码更简洁,JVM 层面持续优化(锁升级、锁消除、锁粗化)。需要超时获取、可中断、公平锁、多条件队列这些高级功能时用 ReentrantLock。
七、自旋锁
自旋锁的核心思路就是:拿不到锁?不死等,先 "原地转圈" 试几次。
// 自旋锁的简单实现
class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
// CAS 自旋:如果 owner 为 null 就设为当前线程,否则一直循环
while (!owner.compareAndSet(null, current)) {
// 自旋等待
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
好处是避免了线程挂起/恢复的内核态切换开销。坏处是如果锁被持有的时间长,自旋会白白消耗 CPU。所以适合 锁持有时间短 的场景。
八、其他常见锁一览
| 锁类型 | 说明 | 典型代表 |
|---|---|---|
| 读写锁 | 读读共享,读写互斥,写写互斥 | ReentrantReadWriteLock |
| 分段锁 | 把数据分段,每段独立加锁,提高并发度 | ConcurrentHashMap(JDK 7 的 Segment) |
| 死锁 | 两个线程互相持有对方需要的锁,永久等待 | —(这是需要避免的问题) |
ConcurrentHashMap 在 JDK 7 中用 Segment 分段锁,每个 Segment 是一把 ReentrantLock,默认 16 段,理论上支持 16 个线程并发写。JDK 8 改为 CAS + synchronized(对每个桶的头节点加锁),并发度从 "段数" 提升到 "桶数",粒度更细。
面试高频追问
-
synchronized和ReentrantLock怎么选?- 简单同步用
synchronized(代码简洁、JVM 持续优化),需要高级功能(可中断、超时、公平、多条件)用ReentrantLock。
- 简单同步用
-
什么是 CAS?有什么问题?
- Compare And Swap,乐观锁的底层实现。问题有三个:ABA 问题(用
AtomicStampedReference解决)、自旋开销(竞争激烈时 CPU 空转)、只能保证单个变量的原子性(用AtomicReference包装多个变量)。
- Compare And Swap,乐观锁的底层实现。问题有三个:ABA 问题(用
-
什么是锁消除和锁粗化?
- 锁消除是 JIT 优化——检测到不可能被共享的对象上的
synchronized会自动消除。锁粗化是 JIT 把多次连续的加锁解锁合并为一次,减少开销。
- 锁消除是 JIT 优化——检测到不可能被共享的对象上的
-
synchronized锁升级能降级吗?- 正常情况下不能,只升不降。但在 STW(全局安全点)期间,JVM 允许做降级优化。
常见面试变体
- "说说
synchronized和ReentrantLock的区别?" - "什么是 CAS?ABA 问题怎么解决?"
- "
synchronized的锁升级过程是怎样的?" - "什么是公平锁?
synchronized是公平锁吗?"
记忆口诀
锁分类六大维度:思想(悲观/乐观)、策略(公平/非公平)、持有(独享/共享)、重入(可/不可)、状态(偏向→轻量→重量)、实现(关键字/API)。
总结
Java 锁的分类不是孤立的,同一个锁从不同维度看会有不同 "身份"。比如 synchronized 是悲观锁、非公平锁、独享锁、可重入锁,还支持锁升级。面试时先给出分类体系的全景图,再挑 2-3 个重点维度深入展开(推荐悲观锁/乐观锁和锁升级),面试官就知道你是有体系地掌握了。