说一下Java中的锁
Java 中的“锁”是一个非常大的话题,可以从不同维度进行分类。下面我按面试常问的脉络帮你梳理一遍,从基础概念到进阶原理。
一、按锁的性质/行为分类
1. 乐观锁 vs 悲观锁
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 核心思想 | 认为并发冲突一定会发生,先加锁再操作 | 认为冲突很少发生,操作时不加锁,提交时再校验 |
| 实现方式 | synchronized、ReentrantLock、数据库 select ... for update |
CAS(Compare And Swap)、版本号机制 |
| 适用场景 | 写多读少、冲突激烈 | 读多写少、冲突较少 |
| 缺点 | 加锁释放锁开销大,可能死锁 | 失败重试开销大,ABA 问题 |
ABA 问题:用 CAS 时,值从 A 改成 B 又改回 A,CAS 检查不出变化。可用
AtomicStampedReference加版本号解决。
2. 公平锁 vs 非公平锁
- 公平锁:线程按申请锁的顺序获取锁,先来先得。
new ReentrantLock(true)。 - 非公平锁:允许插队,新来的线程可能直接抢到锁。
synchronized和new ReentrantLock()默认都是非公平。
为什么默认非公平? 非公平锁吞吐量更高。因为唤醒一个排队线程需要上下文切换,而新来的线程可能刚好在 CPU 上,直接拿到锁能减少切换开销。
3. 可重入锁 vs 不可重入锁
- 可重入锁:同一线程可以多次获取同一把锁,不会死锁。
synchronized和ReentrantLock都是可重入的。 - 不可重入锁:同一线程再次获取锁会被阻塞。早期的一些自研锁可能是这样。
可重入的实现:锁内部维护持有线程和重入次数,每次获取 +1,释放 -1,减到 0 才真正释放。
4. 独占锁 vs 共享锁
- 独占锁(排他锁):同一时刻只允许一个线程持有。
ReentrantLock、synchronized。 - 共享锁:同一时刻允许多个线程持有。
ReentrantReadWriteLock的读锁、Semaphore、CountDownLatch。
ReentrantReadWriteLock 就是读写分离:读读不互斥,读写互斥,写写互斥。
二、按 JVM 实现层面分类
5. 偏向锁 → 轻量级锁 → 重量级锁(锁升级)
这是 synchronized 在 HotSpot 中的优化路径,锁状态记录在对象头的 Mark Word 中:
| 锁状态 | 适用场景 | 原理 |
|---|---|---|
| 无锁 | 无并发 | — |
| 偏向锁 | 只有一个线程访问 | Mark Word 记录线程 ID,同一线程进入不需 CAS |
| 轻量级锁 | 少量线程交替执行 | CAS 自旋尝试获取锁,不阻塞 |
| 重量级锁 | 多线程激烈竞争 | 依赖操作系统互斥量(mutex),线程阻塞挂起 |
升级过程不可逆(偏向锁可批量撤销,但整体是单向升级)。
JDK 15 之后偏向锁默认禁用,JDK 18 彻底移除。因为现代应用并发高,偏向锁收益有限且维护成本大。
6. 自旋锁 vs 自适应自旋锁
- 自旋锁:线程获取不到锁时不立即阻塞,而是循环重试(自旋),避免上下文切换。
- 自适应自旋:JVM 根据上次自旋成功率动态调整自旋次数。成功过就多转几次,总失败就少转或不转。
自旋适合锁持有时间短的场景,否则空转浪费 CPU。
三、JDK 并发包(JUC)中的具体锁实现
7. AQS(AbstractQueuedSynchronizer)
JUC 锁的基石,内部维护:
- 一个
volatile int state表示同步状态 - 一个 CLH 双向队列存放等待线程
ConditionObject支持条件等待
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS 实现。
8. ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 必须手动释放
}
特点:
- 可重入、可中断(
lockInterruptibly)、可超时(tryLock(timeout)) - 支持公平/非公平
- 支持多个条件变量
Condition
与 synchronized 对比:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置 | JDK API(AQS) |
| 释放方式 | 自动释放 | 手动 finally 释放 |
| 可中断 | 否 | 是 |
| 超时获取 | 否 | 是 |
| 公平锁 | 否 | 可选 |
| 条件变量 | 单个 wait/notify | 多个 Condition |
9. ReentrantReadWriteLock
读写分离,提升读多写少场景性能:
- 读锁共享,写锁独占
- 支持锁降级(写锁 → 读锁),不支持锁升级
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
rwLock.writeLock().lock();
10. StampedLock(JDK 8+)
比 ReentrantReadWriteLock 性能更好的读写锁,支持三种模式:
- 写锁:独占
- 悲观读锁:共享
- 乐观读:不加锁,读取后校验 stamp 是否变化
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// 读操作
if (!lock.validate(stamp)) {
stamp = lock.readLock(); // 升级为悲观读
try { /* 重新读 */ } finally { lock.unlockRead(stamp); }
}
注意:StampedLock 不可重入,且不支持 Condition。
11. 其他同步工具(广义的“锁”)
| 工具 | 作用 |
|---|---|
Semaphore |
信号量,控制同时访问的线程数 |
CountDownLatch |
倒计时门闩,等待 N 个任务完成 |
CyclicBarrier |
循环栅栏,一组线程互相等待到齐 |
Phaser |
更灵活的多阶段同步 |
四、面试高频追问点
- synchronized 和 Lock 的区别? → 见上表
- synchronized 锁升级过程? → 偏向 → 轻量 → 重量
- AQS 原理? → state + CLH 队列 + CAS
- CAS 的缺点? → ABA 问题、自旋开销、只能保证一个变量原子性
- 死锁的四个必要条件? → 互斥、请求保持、不可剥夺、循环等待
- 如何避免死锁? → 按序加锁、超时释放、一次性申请所有资源
- ReentrantLock 公平锁怎么实现? → AQS 中
hasQueuedPredecessors()判断是否有前驱节点
五、一句话总结
Java 的锁可以从 乐观/悲观、公平/非公平、可重入/不可重入、独占/共享 等维度理解;JVM 层面
synchronized有锁升级优化;JUC 层面以 AQS 为核心,衍生出ReentrantLock、ReentrantReadWriteLock、StampedLock等实现,配合Semaphore、CountDownLatch等工具应对不同并发场景。
如果你需要我针对某一块(比如 AQS 源码、锁升级细节、死锁排查)展开讲,可以告诉我。

浙公网安备 33010602011771号