📌 微服务架构 :基于Spring Cloud Alibaba的分布式事务处理:Seata AT模式与Sentinel协同实现高并发下数据最终一致性
第7题:说说 Lock 锁?
📚 回答:
- 核心考点 :
Lock是 JUC 包的核心接口,大厂面试不会只问"有哪些方法",而是深入考察 AQS(AbstractQueuedSynchronizer)框架原理 、ReentrantLock 的公平/非公平实现差异 、Condition 条件队列与 Object wait/notify 的本质区别 、读写锁的锁降级与锁升级策略 ,以及 StampedLock 的乐观读模式。面试官真正想判断的是:你是否理解 Lock 体系从"API 层"到"AQS 框架层"再到"CAS + 队列层"的完整架构,以及能否在生产环境中正确选型。
1. Lock 接口体系与核心方法
java.util.concurrent.locks.Lock 是 Java 显式锁的根接口,定义了与 synchronized 不同的锁范式 citation:1:
| 方法 | 功能 | 与 synchronized 对比 |
|---|---|---|
lock() |
阻塞获取锁,不可中断 | synchronized 等价 |
lockInterruptibly() |
阻塞获取锁,可响应中断 | synchronized 不支持 |
tryLock() |
非阻塞尝试获取,立即返回 boolean | synchronized 不支持 |
tryLock(time, unit) |
限时阻塞获取,超时返回 false | synchronized 不支持 |
unlock() |
释放锁(必须在 finally 中调用) | synchronized 自动释放 |
newCondition() |
创建条件队列,支持多条件等待 | synchronized 只有一个 wait/notify |
关键差异 :synchronized 是"隐式锁"(JVM 自动管理),Lock 是"显式锁"(开发者手动控制),提供了更精细的并发控制能力 citation:1。
2. ReentrantLock 的底层实现------AQS 框架
-
2.1 AQS 核心架构
ReentrantLock的底层依赖 AbstractQueuedSynchronizer(AQS),这是 JUC 包中几乎所有同步组件的基石(ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 等)citation:2。AQS 核心结构 citation:2citation:3:
┌─────────────────────────────────────────┐ │ AbstractQueuedSynchronizer │ ├─────────────────────────────────────────┤ │ volatile int state // 同步状态 │ │ volatile Node head // 队列头节点 │ │ volatile Node tail // 队列尾节点 │ │ Thread exclusiveOwnerThread // 独占锁持有者 │ └─────────────────────────────────────────┘state 字段的语义:
ReentrantLock:0 表示无锁,>0 表示重入次数;CountDownLatch:初始值为计数,减到 0 时唤醒等待线程;Semaphore:剩余可用许可数。
Node 节点结构:
┌─────────────────────────────────────────┐ │ Node (双向链表) │ ├─────────────────────────────────────────┤ │ volatile int waitStatus // 节点状态 │ │ volatile Node prev // 前驱节点 │ │ volatile Node next // 后继节点 │ │ volatile Thread thread // 绑定的线程 │ │ Node nextWaiter // 条件队列链接 │ └─────────────────────────────────────────┘waitStatus 值 含义 CANCELLED 1 节点已取消(超时或中断) SIGNAL -1 后继节点需要被唤醒 CONDITION -2 节点在条件队列中 PROPAGATE -3 共享模式下向后传播唤醒 -
2.2 非公平锁的获取流程
java// ReentrantLock.NonfairSync.lock() final void lock() { if (compareAndSetState(0, 1)) // ① 直接 CAS 尝试获取 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // ② CAS 失败,进入 AQS 队列 }acquire(1)的核心逻辑 citation:2:- tryAcquire(1):再次尝试获取(检查 state 是否为 0,或是否重入);
- addWaiter(Node.EXCLUSIVE):创建 Node 节点,CAS 加入队列尾部;
- acquireQueued():自旋/CAS 检查前驱是否为头节点,是则尝试获取锁;
- park() :自旋失败则调用
LockSupport.park()挂起线程。
非公平性体现 :新线程到达时先 CAS 尝试(不排队),失败后才进入队列。这允许"插队",吞吐量更高但可能饥饿 citation:3。
-
2.3 公平锁的获取流程
java// ReentrantLock.FairSync.lock() final void lock() { acquire(1); // 直接走 acquire,没有前置 CAS } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // ← 关键:检查是否有前驱节点 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // ... 重入逻辑 }公平性体现 :
hasQueuedPredecessors()检查队列中是否有等待线程,有则不允许插队,必须排队 citation:3。 -
2.4 锁释放流程
java// ReentrantLock.unlock() → AQS.release(1) public final boolean release(int arg) { if (tryRelease(arg)) { // state 减 1,检查是否为 0 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒头节点的后继 return true; } return false; }关键设计 :释放锁时只唤醒一个 后继线程(
unparkSuccessor),被唤醒的线程继续自旋竞争锁。这是"独占模式"的设计 citation:2。
3. 公平锁 vs 非公平锁深度对比
| 对比维度 | 非公平锁(默认) | 公平锁 |
|---|---|---|
| 获取策略 | 新线程先 CAS 尝试,失败再排队 | 必须检查队列,有等待者则排队 |
| 吞吐量 | 高(减少线程切换) | 低(严格排队,切换频繁) |
| 饥饿风险 | 存在(新线程可能一直插队) | 无(严格 FIFO) |
| 适用场景 | 高并发、短临界区 | 长临界区、需要避免饥饿 |
| 性能差距 | 比公平锁高 10%~20% | 相对低 |
压测数据:在 100 线程竞争下,非公平锁的吞吐量约为公平锁的 1.2~1.5 倍 citation:3。
4. Condition 条件队列------多条件等待/唤醒
-
4.1 与 Object wait/notify 的本质区别
synchronized只有一个隐式条件队列(waitSet),而ReentrantLock可以创建多个Condition,实现更精细的线程协作 citation:1。特性 Object wait/notify Condition await/signal 条件队列数量 1 个(每个对象一个) 多个(每个 Lock 可创建多个) 唤醒精度 notify()随机唤醒一个,notifyAll()全部唤醒signal()唤醒一个,signalAll()全部唤醒,可精确控制使用前提 必须持有对象锁 必须持有 Lock 锁 中断响应 不区分中断原因 awaitUninterruptibly()可选 -
4.2 Condition 的底层实现
每个
Condition维护一个独立的条件队列(单向链表),与 AQS 的主队列(同步队列)分离 citation:2:Lock (AQS) ├── 同步队列(Sync Queue):双向链表,等待锁的线程 │ head → Node(T1) ↔ Node(T2) ↔ Node(T3) → tail │ ├── Condition1 条件队列(Wait Queue):单向链表 │ firstWaiter → Node(T4) → Node(T5) → null │ └── Condition2 条件队列(Wait Queue):单向链表 firstWaiter → Node(T6) → nullawait() 流程 citation:2:
- 释放 Lock(
fullyRelease),保存重入次数; - 创建 Node 加入 Condition 的条件队列;
park()挂起线程;- 被
signal()唤醒后,从条件队列转移到同步队列,重新竞争 Lock。
signal() 流程 citation:2:
- 检查是否持有 Lock;
- 从条件队列头部取出一个 Node;
- 将其转移到同步队列尾部;
- 设置前驱节点状态为 SIGNAL,唤醒该线程。
- 释放 Lock(
-
4.3 经典示例------生产者消费者(双条件)
javapublic class BoundedBuffer<T> { private final Lock 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 void put(T item) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // 队列满,等待"不满"信号 } queue.add(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是两个独立的条件队列,生产者只唤醒消费者,消费者只唤醒生产者,避免了notifyAll()的"惊群效应" citation:1。
5. ReentrantReadWriteLock------读写分离
-
5.1 设计动机
读操作不修改数据,多个读线程可以并行;写操作需要独占。
ReentrantReadWriteLock将锁拆分为读锁 (共享)和写锁(独占),提升读多写少场景的并发度 citation:4。锁类型 获取条件 并发性 读锁 ( readLock())无写锁或写锁由当前线程持有 多个读线程可同时持有 写锁 ( writeLock())无读锁且无其他写锁 独占 -
5.2 AQS 中的实现
AQS 的
state被拆分为高 16 位(读锁计数)和低 16 位(写锁重入次数):state (32 bit) ├─ 高 16 bit:读锁持有数(包括重入) └─ 低 16 bit:写锁重入次数(0 表示无写锁)读锁获取 :检查低 16 位是否为 0(无写锁),是则高 16 位 +1;
写锁获取:检查高 16 位是否为 0(无读锁)且低 16 位为 0 或当前线程持有,是则低 16 位 +1 citation:4。
-
5.3 锁降级(Lock Downgrading)
javaReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock rl = rwl.readLock(); ReentrantReadWriteLock.WriteLock wl = rwl.writeLock(); wl.lock(); try { // 修改数据 data = newValue; // 锁降级:在释放写锁前获取读锁 rl.lock(); // ← 允许!当前线程持有写锁时可直接获取读锁 } finally { wl.unlock(); // 释放写锁,此时持有读锁 } try { // 使用数据(保证看到最新值,同时允许其他读线程并行) use(data); } finally { rl.unlock(); }作用:保证数据修改后的可见性,同时降低锁粒度,允许其他读线程并行 citation:4。
⚠️ 注意 :不支持锁升级(持有读锁时获取写锁会导致死锁)。
6. StampedLock------乐观读与性能优化
JDK 8 引入的 StampedLock,在读多写少场景下性能优于 ReentrantReadWriteLock citation:5。
| 模式 | 方法 | 特点 |
|---|---|---|
| 写锁 | writeLock() |
独占,与读写锁类似 |
| 悲观读锁 | readLock() |
共享,与读写锁类似 |
| 乐观读 | tryOptimisticRead() |
无锁读取,返回 stamp,验证失败再转悲观读 |
乐观读示例 citation:5:
java
public class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 乐观读
public double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // ① 获取乐观读 stamp(无锁)
double currentX = x, currentY = y; // ② 拷贝变量
if (!sl.validate(stamp)) { // ③ 验证 stamp 是否被写操作修改
stamp = sl.readLock(); // ④ 失败,转为悲观读锁
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
原理:乐观读不真正获取锁,只是记录一个版本戳(stamp)。读取完成后验证 stamp,如果期间没有写操作,直接返回结果;如果有写操作,升级为悲观读锁重试 citation:5。
性能对比:在纯读场景下,StampedLock 的乐观读性能接近无锁,远超 ReentrantReadWriteLock。
7. Lock 与 synchronized 的选型决策
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单同步代码块 | synchronized |
语法简洁,JVM 自动优化(偏向锁、轻量级锁) |
| 需要超时获取 | ReentrantLock.tryLock(time, unit) |
避免无限等待 |
| 需要响应中断 | ReentrantLock.lockInterruptibly() |
可中断阻塞 |
| 需要公平锁 | ReentrantLock(true) |
避免饥饿 |
| 需要多条件等待 | ReentrantLock + Condition |
精确唤醒,避免惊群 |
| 读多写少 | ReentrantReadWriteLock |
读锁共享,提升并发 |
| 读极多写极少 | StampedLock(乐观读) |
无锁读,性能最高 |
| 高并发计数 | LongAdder |
分段累加,无锁 |
8. 生产环境避坑指南
-
8.1 必须在 finally 中 unlock
java// ❌ 错误:异常时锁不释放 lock.lock(); doSomething(); // 如果抛异常,锁永远不释放 lock.unlock(); // ✅ 正确 lock.lock(); try { doSomething(); } finally { lock.unlock(); // 确保释放 } -
8.2 避免锁顺序导致的死锁
java// ❌ 错误:不同顺序获取锁 // 线程 A:lock1.lock(); lock2.lock(); // 线程 B:lock2.lock(); lock1.lock(); // ✅ 正确:全局统一顺序 // 所有线程都按 lock1 → lock2 的顺序获取 -
8.3 注意 tryLock 的返回值
java// ❌ 错误:忽略返回值 lock.tryLock(); doSomething(); // 如果没获取到锁,也在执行! // ✅ 正确 if (lock.tryLock()) { try { doSomething(); } finally { lock.unlock(); } } -
8.4 StampedLock 不支持重入
StampedLock不是可重入锁,同一线程重复获取会导致死锁 citation:5。 -
8.5 读写锁的写锁饥饿
ReentrantReadWriteLock默认非公平模式下,读线程可能持续涌入,导致写线程长期等待(写饥饿)。可通过ReentrantReadWriteLock(true)使用公平模式,或限制读锁持有时间 citation:4。
9. 面试官追问与高分回答模板
-
追问 1:"说说 Lock 锁?"
低分回答:"Lock 是显式锁,需要手动 lock/unlock,支持公平锁、可中断、超时。"(太浅)
高分回答:
"
Lock是 JUC 包的显式锁接口,与synchronized相比提供了更精细的并发控制能力:- 可中断 :
lockInterruptibly()允许在等待锁时响应中断; - 超时获取 :
tryLock(time, unit)避免无限等待; - 公平性:可选公平/非公平模式;
- 多条件队列 :
newCondition()创建多个条件队列,精确唤醒。
底层实现上,ReentrantLock依赖 AQS 框架,通过 CAS + 自旋 + 队列实现锁的获取与释放。AQS 的state字段表示同步状态,Node双向链表维护等待队列。
选型上,简单场景用synchronized,需要超时/中断/多条件时用ReentrantLock,读多写少用ReentrantReadWriteLock,读极多用StampedLock。" citation:1citation:2
- 可中断 :
-
追问 2:"ReentrantLock 的公平锁和非公平锁底层怎么实现的?"
高分回答:
"两者都基于 AQS 框架,差异在
tryAcquire方法:- 非公平锁(默认) :新线程到达时先 CAS 尝试获取锁 (
compareAndSetState(0, 1)),成功则直接获取,失败才进入 AQS 队列。这允许'插队',吞吐量高但可能饥饿。 - 公平锁 :
tryAcquire中先调用hasQueuedPredecessors()检查队列中是否有等待线程。如果有,即使state为 0 也不允许获取 ,必须排队。
非公平锁性能更高(减少线程切换),但公平锁避免饥饿。压测显示非公平锁吞吐量比公平锁高 10%~20%。" citation:2citation:3
- 非公平锁(默认) :新线程到达时先 CAS 尝试获取锁 (
-
追问 3:"Condition 和 Object 的 wait/notify 有什么区别?"
高分回答:
"核心区别有三点:
- 队列数量 :
synchronized每个对象只有一个隐式 waitSet;ReentrantLock可以创建多个Condition,每个有独立的条件队列。 - 唤醒精度 :
notify()随机唤醒一个,notifyAll()全部唤醒;signal()唤醒一个,signalAll()全部唤醒,且可以精确控制唤醒哪个条件的线程。 - 底层实现 :
wait()将线程加入对象头的 Monitor 的_WaitSet;await()将线程加入Condition的条件队列 (与 AQS 主队列分离),被signal()后转移到 AQS 主队列重新竞争锁。
经典应用是生产者消费者模型:用notFull和notEmpty两个 Condition,生产者只唤醒消费者,消费者只唤醒生产者,避免notifyAll()的惊群效应。" citation:1citation:2
- 队列数量 :
-
追问 4:"AQS 是什么?它的核心设计是什么?"
高分回答:
"AQS(AbstractQueuedSynchronizer)是 JUC 包的并发框架基石,
ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock都基于它实现。核心设计是 '状态 + 队列' 模式:
- state 字段 :
volatile int表示同步状态,具体语义由子类定义(ReentrantLock 的重入次数、CountDownLatch 的剩余计数等); - FIFO 队列 :
Node双向链表维护等待线程,头节点是持有锁的线程(或虚拟节点),后续节点自旋/CAS 检查前驱; - 模板方法模式 :AQS 定义了
acquire/release框架,子类只需实现tryAcquire/tryRelease等钩子方法。
获取锁时:CAS state → 失败则加入队列尾部 → 自旋检查前驱 → 失败则LockSupport.park()挂起。释放锁时:修改 state → 唤醒后继节点。" citation:2citation:3
- state 字段 :
-
追问 5:"StampedLock 的乐观读是什么?有什么使用限制?"
高分回答:
"
StampedLock的乐观读是一种无锁读取机制:- 调用
tryOptimisticRead()获取一个版本戳(stamp),不真正加锁; - 拷贝需要读取的变量;
- 调用
validate(stamp)检查 stamp 是否被写操作修改; - 如果未修改,直接返回结果;如果修改了,升级为悲观读锁(
readLock())重试。
优势 :纯读场景下性能接近无锁,远超ReentrantReadWriteLock。
限制: - 不可重入:同一线程重复获取会导致死锁;
- 不支持条件队列 :没有
newCondition()方法; - stamp 必须验证 :忘记
validate()会导致读到脏数据; - 写锁饥饿:乐观读不阻塞写线程,但大量乐观读可能导致写线程长期等待。" citation:5
- 调用
-
追问 6:"ReentrantReadWriteLock 的锁降级是什么?为什么需要?"
高分回答:
"锁降级是指持有写锁的线程,在释放写锁前获取读锁,然后释放写锁,继续持有读锁。
javawl.lock(); try { data = newValue; // 修改数据 rl.lock(); // 锁降级:写锁内获取读锁 } finally { wl.unlock(); // 释放写锁,此时仍持有读锁 } try { use(data); // 使用数据,允许其他读线程并行 } finally { rl.unlock(); }为什么需要?
- 保证可见性:写锁释放后立即可见最新数据,但如果不持有读锁,其他写线程可能立即获取写锁修改数据;
- 降低锁粒度:持有读锁时允许其他读线程并行,提升并发度;
- 替代 volatile :某些场景下锁降级比 volatile + 写锁更安全。
注意 :ReentrantReadWriteLock不支持锁升级(读锁内获取写锁会导致死锁)。" citation:4
10. 方案选型速查表
| 业务场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 简单同步 | synchronized |
语法简洁,JVM 自动优化 |
| 需要超时/中断 | ReentrantLock |
tryLock/lockInterruptibly |
| 需要公平锁 | ReentrantLock(true) |
避免饥饿 |
| 生产者消费者(多条件) | ReentrantLock + Condition |
精确唤醒,避免惊群 |
| 读多写少 | ReentrantReadWriteLock |
读锁共享 |
| 读极多写极少 | StampedLock(乐观读) |
无锁读,性能最高 |
| 缓存(读多写少) | ReentrantReadWriteLock |
锁降级保证可见性 |
| 高并发计数 | LongAdder |
分段累加,无锁 |
💡 面试官想要的满分总结:
Lock体系是 Java 并发编程从"语法糖"走向"精细化控制"的标志。理解 Lock 必须抓住三条主线:第一条:AQS 框架 。
ReentrantLock、CountDownLatch、Semaphore等几乎所有 JUC 同步组件都基于 AQS 实现。核心设计是 state(同步状态)+ Node 队列(FIFO 等待队列)+ CAS + 自旋 + park/unpark。掌握 AQS 就掌握了 JUC 的半壁江山。第二条:公平与非公平的权衡。非公平锁允许"插队"(先 CAS 尝试),吞吐量高但可能饥饿;公平锁严格 FIFO,避免饥饿但切换开销大。默认非公平是工程实践的最优解。
第三条:Condition 的多条件队列 。与
synchronized的单 waitSet 相比,Condition允许创建多个独立条件队列,实现精确唤醒(生产者只唤醒消费者),避免notifyAll()的惊群效应。选型上,简单场景用
synchronized,需要高级功能用ReentrantLock,读多写少用ReentrantReadWriteLock,读极多用StampedLock。永远记住:先保证正确性(finally 中 unlock),再追求性能(乐观读、锁降级)。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯