Lock 体系
一、基础认知
1.1 Lock 接口核心方法
Lock 是 JUC 包中锁的顶层接口,定义了以下核心方法:
public interface Lock {
void lock(); // 获取锁(阻塞,直到获取成功)
void lockInterruptibly(); // 可中断地获取锁(阻塞时可以被中断)
boolean tryLock(); // 尝试获取锁(立即返回,成功 true / 失败 false)
boolean tryLock(long time, TimeUnit unit); // 带超时的尝试获取
void unlock(); // 释放锁
Condition newCondition(); // 创建条件队列
}
使用铁律:lock()/unlock() 必须配对,unlock() 必须在 finally 中调用。
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 保证锁一定能释放
}
1.2 Lock 与 synchronized 对比(面试必问)
| 对比维度 | synchronized | Lock(ReentrantLock) |
|---|---|---|
| 关键字 vs API | JVM 关键字 | Java API 接口 |
| 锁获取/释放 | JVM 自动释放 | 手动 lock/unlock,finally 中释放 |
| 可中断性 | ❌ 不能响应中断 | ✅ lockInterruptibly() 支持 |
| 超时获取 | ❌ 不支持 | ✅ tryLock(time, unit) 支持 |
| 公平性 | ❌ 非公平 | ✅ 支持公平/非公平构造 |
| 多个条件 | ❌ 一个 wait 队列 | ✅ 多个 Condition 精准唤醒 |
| 锁状态检测 | ❌ 无法检测 | ✅ isLocked() / isHeldByCurrentThread() |
| 性能 | JDK 6+ 优化后与 Lock 相当 | 同级别 |
| 编码复杂度 | 简单 | 较复杂(需手动释放) |
选型建议:简单场景优先用 synchronized,需要超时/中断/多条件时用 Lock。
二、核心锁实现
2.1 ReentrantLock(可重入锁)
什么是可重入?
同一个线程可以多次获取同一把锁,不会产生死锁。每次 lock() 重入计数 +1,每次 unlock() 计数 -1,减到 0 才真正释放。
public void outer() {
lock.lock();
try {
inner(); // 同一个线程可以再次获取锁
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
// 即使 outer 已持锁,这里也能获取成功(可重入)
} finally {
lock.unlock();
}
}
公平模式 vs 非公平模式
// 非公平锁(默认):线程先抢锁,抢不到才排队
ReentrantLock unfairLock = new ReentrantLock(false);
// 公平锁:FIFO 排队,先来的先得
ReentrantLock fairLock = new ReentrantLock(true);
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 非公平(默认) | 吞吐量高,减少线程切换 | 可能线程饥饿 | 通用场景,吞吐量优先 |
| 公平 | 等待时间公平,无饥饿 | 吞吐量低(约 1/3) | 要求公平调度、持有锁时间长的场景 |
底层区别(基于 AQS):
- 非公平锁:
lock()时先 CAS 抢一次锁,抢不到才走 acquire 流程 - 公平锁:
tryAcquire()先检查hasQueuedPredecessors(),CLH 队列中有前驱则排队
2.2 ReentrantReadWriteLock(读写分离锁)
读写规则
读锁(共享锁) + 读锁(共享锁) = ✅ 允许多线程并发读
读锁(共享锁) + 写锁(独占锁) = ❌ 读写互斥
写锁(独占锁) + 写锁(独占锁) = ❌ 写写互斥
核心应用:缓存
public class CacheService {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private final Map<String, Object> cache = new HashMap<>();
// 读 — 多个线程并发读
public Object get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写 — 独占
public void put(String key, Object value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// 锁降级:写 → 读(保证写完后能继续读)
public void updateAndRead(String key, Object value) {
writeLock.lock();
try {
cache.put(key, value);
readLock.lock(); // 写锁持有中,获取读锁成功(锁降级)
} finally {
writeLock.unlock(); // 释放写锁,降级为读锁状态
}
try {
Object val = cache.get(key); // 此时仍持有读锁,数据安全
} finally {
readLock.unlock();
}
}
}
注意事项
- 锁降级(写→读):✅ 允许,写线程可以降级为读线程,保证数据可见性
- 锁升级(读→写):❌ 不允许,多个读线程同时尝试升级写锁会导致死锁
- 写锁饥饿:大量读线程持续持有读锁时,写线程可能长时间无法获取写锁
- 适用场景:读多写少(缓存、配置中心、路由表)
2.3 StampedLock(JDK 8 邮戳锁)
为什么要 StampedLock?
ReentrantReadWriteLock 存在两个问题:
- 写饥饿:读线程多时写线程难以获取写锁
- 悲观读:读的时候强制加锁,影响了读性能
StampedLock 通过乐观读解决这两个问题。
三种锁模式
| 模式 | 方法 | 行为 | 性能 |
|---|---|---|---|
| 写锁 | writeLock() | 独占,全部互斥 | 低 |
| 悲观读锁 | readLock() | 共享锁,和写锁互斥 | 中 |
| 乐观读 | tryOptimisticRead() | 不加锁,仅返回 stamp 戳,需 validate() 验证 | 最高 |
乐观读核心用法
public class PointService {
private final StampedLock stampedLock = new StampedLock();
private int x = 0, y = 0;
// 写操作 — 独占
public void move(int dx, int dy) {
long stamp = stampedLock.writeLock();
try {
x += dx;
y += dy;
} finally {
stampedLock.unlockWrite(stamp);
}
}
// 乐观读 — 不加锁,性能极高
public int readOptimistic() {
// ① 乐观读:拿到 stamp 戳
long stamp = stampedLock.tryOptimisticRead();
int curX = x, curY = y;
// ② 验证:数据有没有被写线程修改过?
if (!stampedLock.validate(stamp)) {
// ③ 被修改了,升级为悲观读锁
stamp = stampedLock.readLock();
try {
curX = x;
curY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return curX + curY;
}
}
乐观读的原理: 读取时不加任何锁(不阻塞任何线程),读完验证 stamp 戳有无变化。无变化则读取有效;有变化则退化为悲观读。
注意事项
| 特性 | 说明 |
|---|---|
| 不可重入 | 同一个线程不能多次获取同一把锁,重入会导致死锁 |
| 不支持 Condition | newCondition() 抛 UnsupportedOperationException |
| 乐观读不是锁 | 必须配合 validate() 验证,否则可能读到脏数据 |
| 中断可能 CPU 飙升 | 内部使用自旋锁实现,中断操作可能造成自旋 |
| 适用场景 | 读远多于写、超高并发、对数据一致性要求不极端 |
三、等待唤醒机制(Condition)
3.1 Condition 是什么?
Condition 将 等待/唤醒 与 锁 绑定,实现精准的线程间通信。
| 对比 | Object wait/notify | Condition await/signal |
|---|---|---|
| 绑定关系 | 绑定 synchronized | 绑定 Lock |
| 条件队列数量 | 一个对象一个 wait 队列 | 一个 Lock 可以创建多个 Condition |
| 唤醒粒度 | notify() 随机唤醒一个 | signal() 精准唤醒特定条件的线程 |
| 中断响应 | 抛 InterruptedException | 抛 InterruptedException |
3.2 多条件精准唤醒(经典:生产者-消费者)
public class BoundedQueue<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 不满条件
private final Condition notEmpty = lock.newCondition(); // 不空条件
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public BoundedQueue(int capacity) {
this.capacity = capacity;
}
// 生产者:队列满则等待「不满」信号
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满了,等待不满条件
}
queue.offer(item);
notEmpty.signal(); // 通知消费者:队列不空了
} finally {
lock.unlock();
}
}
// 消费者:队列空则等待「不空」信号
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 队列空了,等待不空条件
}
T item = queue.poll();
notFull.signal(); // 通知生产者:队列不满了
return item;
} finally {
lock.unlock();
}
}
}
优势: 用 notFull 和 notEmpty 两个条件分开等待,不会出现用 synchronized 时 notifyAll() 唤醒了不该唤醒的线程的问题。
3.3 await/signal 使用规范
// await() 必须在循环中检查条件(防止假唤醒)
while (条件不满足) {
condition.await(); // 不是 if,是 while!
}
// signal() 必须在条件满足时调用
if (条件满足) {
condition.signal(); // 或 signalAll()
}
| 方法 | 行为 |
|---|---|
await() |
释放锁,进入条件队列等待 |
await(time, unit) |
超时等待 |
signal() |
唤醒条件队列中的一个线程(移入同步队列竞争锁) |
signalAll() |
唤醒条件队列中所有线程 |
四、底层基础
4.1 LockSupport(线程阻塞/唤醒工具)
核心方法
LockSupport.park(); // 阻塞当前线程
LockSupport.parkNanos(long nanos); // 超时阻塞
LockSupport.unpark(Thread t); // 唤醒指定线程
permit 许可机制
每个线程有一个「许可」(permit),值只能是 0 或 1:
unpark(thread) → 设置 permit = 1
park() → 如果 permit > 0,消耗 permit 并返回
如果 permit = 0,阻塞直到 permit 变为 1
这也是 unpark 可以先于 park 调用 的原因——提前发放 permit,后面的 park() 不会阻塞。
与 wait/notify 对比
| 对比点 | Object.wait/notify | LockSupport.park/unpark |
|---|---|---|
| 是否需要先持有锁 | ✅ 必须在 synchronized 内 | ❌ 完全不需要 |
| 唤醒方式 | notify() 随机唤醒一个 | unpark(thread) 精准唤醒指定线程 |
| 调换顺序 | ❌ notify 先于 wait → wait 永远等不到 | ✅ unpark 先于 park → park 直接返回 |
| 中断行为 | 抛 InterruptedException | 不抛异常,直接返回需自行检查中断标志 |
| 应用 | 所有 Object 对象 | AQS 的基石 |
示例
// 精准唤醒(对比 notify 的随机唤醒优势明显)
Thread worker = new Thread(() -> {
System.out.println("等待任务...");
LockSupport.park();
System.out.println("被唤醒了!");
});
worker.start();
Thread.sleep(1000);
LockSupport.unpark(worker); // 精准唤醒 worker 线程
4.2 AQS 绑定逻辑
Lock 体系所有核心类都依赖 AQS 实现:
ReentrantLock → 内部类 Sync extends AbstractQueuedSynchronizer
ReentrantReadWriteLock → 内部类 Sync extends AbstractQueuedSynchronizer
StampedLock → 自实现 CLH(但思路类似 AQS)
LockSupport → AQS 底层阻塞/唤醒的实现工具
Condition → AQS 内部 ConditionObject 实现
学习路径建议: 先理解 AQS(见 3_aqs.md),再学 Lock 实现会豁然开朗。
五、特性要点
5.1 五大特性总览
| 特性 | 说明 | 涉及类 |
|---|---|---|
| 可重入 | 同一线程可多次获取同一把锁 | ReentrantLock、ReadWriteLock |
| 可中断 | 等待锁时可以被其他线程中断 | ReentrantLock.lockInterruptibly() |
| 可超时 | 指定时间内获取不到就放弃 | ReentrantLock.tryLock(time, unit) |
| 公平性 | 支持公平/非公平两种模式 | ReentrantLock(boolean fair) |
| 多条件 | 一个 Lock 可创建多个 Condition | ReentrantLock.newCondition() |
5.2 锁降级 vs 锁升级
| 概念 | 定义 | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|
| 锁降级 | 写锁 → 读锁 | ✅ 支持 | ✅ 支持 tryConvertToReadLock() |
| 锁升级 | 读锁 → 写锁 | ❌ 死锁风险(多个读线程都要写) | ✅ 支持 tryConvertToWriteLock() |
锁降级的作用: 写完数据后不释放锁就获取读锁,保证写完之后到真正释放读锁这段时间,数据不会被其他线程修改。
5.3 锁饥饿问题
| 锁类型 | 饥饿风险 | 原因 | 解决 |
|---|---|---|---|
| ReentrantLock(非公平) | 有 | 新线程插队 | 用公平锁 |
| ReentrantReadWriteLock | 写锁饥饿 | 大量读线程 | 用 StampedLock |
| StampedLock | 低 | 乐观读不阻塞写 | 乐观读本质无锁 |
六、实战与面试重点
6.1 锁的标准写法(try-finally)
// 模式一:lock + try-finally(最常用)
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
// 模式二:tryLock + 双检查
if (lock.tryLock()) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
// 获取失败的处理
}
// 模式三:lockInterruptibly
lock.lockInterruptibly();
try {
// 业务逻辑(可被中断取消)
} finally {
lock.unlock();
}
6.2 各类锁业务选型
| 业务场景 | 推荐锁 | 原因 |
|---|---|---|
| 简单临界区、代码块 | synchronized | 简洁、自动释放、不易出错 |
| 需要超时等待 | ReentrantLock.tryLock() | 避免死锁 |
| 可中断等待 | ReentrantLock.lockInterruptibly() | 支持取消 |
| 读多写少缓存 | ReentrantReadWriteLock | 读读并发 |
| 超高并发读场景 | StampedLock 乐观读 | 读完全不加锁 |
| 需要多个条件队列 | ReentrantLock + Condition | 精准唤醒 |
| 简单等待通知 | synchronized + wait/notify | 够用 |
6.3 公平/非公平锁底层区别
// === 非公平锁 ===
final void lock() {
// 一进来就 CAS 抢一次,不管队列里有没有人在等
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到才走排队流程
}
// === 公平锁 ===
protected final boolean tryAcquire(int acquires) {
// 先检查队列中是否有前驱在等待
if (!hasQueuedPredecessors() && // 唯一区别!
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
非公平锁为什么吞吐量更高?
- 非公平锁:线程切换次数少(刚释放锁的线程有机会立即再次获取),缓存不失效
- 公平锁:线程切换频繁,上下文切换开销大
6.4 读写锁 vs 邮戳锁场景
| 对比 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 读读并发 | ✅ | ✅ |
| 读写互斥 | ✅ | ✅(悲观读)/ ❌(乐观读) |
| 写饥饿 | ❌ 可能发生 | ✅ 乐观读不阻塞写 |
| 性能 | 中 | 高(乐观读无锁) |
| 可重入 | ✅ | ❌ |
| Condition | ✅ | ❌ |
| 适用 | 缓存、配置 | 超高并发读、数据一致性要求不极端 |
七、常见面试题
Q1:synchronized 和 ReentrantLock 的区别?
见第一章对比表。核心:synchronized 是关键字自动释放,Lock 是 API 手动释放但更灵活(可中断、可超时、多条件)。
Q2:Condition 的 await/signal 和 Object wait/notify 的区别?
| 维度 | wait/notify | await/signal |
|---|---|---|
| 绑定锁 | synchronized | Lock |
| 条件队列数 | 单个 | 多个(精准唤醒) |
| 唤醒 | 随机或全部 | 精准唤醒特定条件 |
| 使用 | 简单 | 灵活 |
Q3:LockSupport.park/unpark 的原理?
基于 permit 许可机制(类似只有 0 或 1 的信号量)。unpark 设置 permit = 1,park 消耗 permit,permmit = 0 则阻塞。无需先持有锁,可以精准唤醒指定线程。
Q4:如何避免死锁?
- 锁排序:所有线程按固定顺序获取锁
- tryLock 超时:获取不到主动放弃
- 缩小锁范围:只在需要时持有锁
- 避免锁嵌套:减少一个方法中持有多个锁
Q5:锁降级和锁升级分别指什么?
- 锁降级(写锁→读锁):写线程获取读锁后释放写锁,保证数据可见性
- 锁升级(读锁→写锁):ReadWriteLock 不支持(死锁风险),StampedLock 支持
Q6:StampedLock 的乐观读为什么比 ReadWriteLock 快?
乐观读不加任何锁,不阻塞任何线程(包括写线程),仅通过验证 stamp 戳判断数据是否有效。消除了读与读、读与写之间的竞争。
Q7:公平锁和非公平锁怎么选?
默认用非公平锁(吞吐量高)。只有需要保证等待时间公平、避免线程饥饿时才用公平锁。
Q8:ReentrantLock 和 StampedLock 的适用场景?
- ReentrantLock:通用互斥场景,需要可重入、Condition
- StampedLock:读远多于写的超高并发场景,能接受不可重入和无 Condition