锁机制与 AQS - 并发 | 炼金塔
锁机制与 AQS
Section titled “锁机制与 AQS”一、引入背景
Section titled “一、引入背景”1.1 为什么要深入理解锁机制?
Section titled “1.1 为什么要深入理解锁机制?”- 面试必考核心:synchronized 锁升级是 Java 并发最高频考点
- 性能优化关键:理解锁的代价才能进行合理优化
- 死锁排查基础:掌握锁机制才能分析并发问题
1.2 实际业务场景痛点
Section titled “1.2 实际业务场景痛点”| 场景 | 痛点 | 解决方案 |
|---|---|---|
| 库存扣减 | 超卖问题 | synchronized / 分布式锁 |
| 账户余额 | 并发更新丢失 | 乐观锁 / CAS |
| 缓存更新 | 读多写少 | 读写锁 |
| 订单状态 | 状态机流转 | 公平锁 |
synchronized 是 Java 内置的互斥锁,基于 JVM 的 Monitor(监视器) 机制实现。
对象头与 Mark Word
Section titled “对象头与 Mark Word”每个 Java 对象的内存布局包含一个 对象头(Object Header),Mark Word 是其中的核心字段(64 位 JVM 中占 8 字节),记录锁状态、GC 信息、哈希码等。
Mark Word 的状态变化(64位):
无锁状态: [unused:25 | hashcode:31 | unused:1 | age:4 | biased:0 | 01]
偏向锁: [thread_id:54 | epoch:2 | unused:1 | age:4 | biased:1 | 01]
轻量级锁: [lock_record_ptr:62 | 00]
重量级锁: [monitor_ptr:62 | 10]
GC 标记: [unused:62 | 11]无锁 │ │ 第一个线程访问同步块 ▼偏向锁(Biased Lock) • Mark Word 记录线程 ID • 同一线程再次进入:只需比较线程 ID,无 CAS 操作 • 代价极低,适合"实际上只有一个线程访问"的场景 │ │ 第二个线程竞争,发起偏向锁撤销(Revoke) ▼轻量级锁(Lightweight Lock) • 竞争线程在各自栈帧创建 Lock Record • CAS 将 Mark Word 替换为 Lock Record 指针 • 失败的线程自旋等待(Adaptive Spinning,自适应自旋) • 代价:自旋消耗 CPU,适合锁持有时间极短的场景 │ │ 自旋超过阈值(默认 10 次)或等待线程数 > 1 ▼重量级锁(Heavyweight Lock) • 操作系统 Mutex,线程进入 BLOCKED 状态 • 涉及用户态↔内核态切换,代价较高 • 适合锁竞争激烈或持有时间长的场景锁消除(Lock Elimination):JIT 编译器通过逃逸分析,发现某个锁对象不可能被多线程访问时,自动去除该锁。
// StringBuffer 是线程安全的,但 sb 是局部变量,不会逃逸// JIT 会消除 append 内部的 synchronizedString concat(String a, String b) { StringBuffer sb = new StringBuffer(); // 不会逃逸 sb.append(a); sb.append(b); return sb.toString();}锁粗化(Lock Coarsening):连续对同一对象加锁,JIT 会将多个锁合并为一个更大范围的锁。
// 原始代码:多次加锁synchronized(lock) { op1(); }synchronized(lock) { op2(); }synchronized(lock) { op3(); }
// JIT 优化后:合并为一次加锁synchronized(lock) { op1(); op2(); op3(); }AQS(AbstractQueuedSynchronizer)
Section titled “AQS(AbstractQueuedSynchronizer)”AQS 是 J.U.C 锁和同步工具的核心框架,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都基于它实现。
AQS 核心数据结构
Section titled “AQS 核心数据结构”AQS 内部结构:
state: int ←── 核心同步状态变量(volatile) ReentrantLock:0=未锁,>0=重入次数 Semaphore:剩余许可数 CountDownLatch:剩余计数
CLH 变体双向队列(等待线程队列):
head → [dummy] ⇄ [Node:Thread-2] ⇄ [Node:Thread-3] ⇄ [Node:Thread-4] ← tail
每个 Node 包含: • thread:等待的线程 • waitStatus:CANCELLED/SIGNAL/CONDITION/PROPAGATE/0 • prev / next:双向链接 • nextWaiter:Condition 队列链接AQS 加锁流程(以 ReentrantLock 为例)
Section titled “AQS 加锁流程(以 ReentrantLock 为例)”lock() 调用 ↓tryAcquire(1) // 子类实现,尝试 CAS 修改 state ├── 成功:设置 exclusiveOwnerThread = 当前线程,返回 └── 失败:↓ addWaiter(Node.EXCLUSIVE) // 将当前线程包装为 Node 加入队列尾部(CAS) ↓ acquireQueued(node, 1) // 自旋等待 ├── 前驱是 head?→ 再次 tryAcquire │ ├── 成功:出队,返回 │ └── 失败:↓ └── shouldParkAfterFailedAcquire?→ LockSupport.park() 挂起 ↑ 释放锁时 LockSupport.unpark(successor) 唤醒后继节点公平锁 vs 非公平锁
Section titled “公平锁 vs 非公平锁”// 非公平锁(默认):新来的线程先 CAS 抢一次,失败才入队final boolean nonfairTryAcquire(int acquires) { if (compareAndSetState(0, acquires)) { // 直接抢!不看队列 setExclusiveOwnerThread(Thread.currentThread()); return true; } // ... 重入检查和入队逻辑}
// 公平锁:检查队列中是否有等待者,有则直接入队protected final boolean tryAcquire(int acquires) { if (!hasQueuedPredecessors() // 队列为空或自己是队首才尝试 CAS && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false;}非公平锁为何性能更好? 减少了线程唤醒→调度→执行的延迟(线程切换开销),刚释放 CPU 的线程大概率能直接再次获取锁,提高了吞吐量。代价是可能导致某些线程长时间等待(饥饿)。
ReentrantLock vs synchronized
Section titled “ReentrantLock vs synchronized”| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 内置,字节码层面 | JDK 类库,Java 代码实现 |
| 锁释放 | 自动(退出同步块) | 必须手动 unlock(),需用 try-finally |
| 可重入 | 支持 | 支持 |
| 公平锁 | 不支持 | 支持(new ReentrantLock(true)) |
| 尝试加锁 | 不支持 | tryLock() / tryLock(time, unit) |
| 可中断等待 | 不支持 | lockInterruptibly() |
| 条件变量 | 单一 wait/notify | 多个 Condition,await()/signal() |
| 性能 | JDK 6+ 锁升级后差距不大 | 高竞争场景略优 |
| 适用建议 | 简单场景,代码简洁 | 需要高级特性时使用 |
// ReentrantLock 标准用法Lock lock = new ReentrantLock();lock.lock();try { // 临界区} finally { lock.unlock(); // 必须在 finally 中释放}
// 多 Condition 示例(有界缓冲区)Lock lock = new ReentrantLock();Condition notFull = lock.newCondition();Condition notEmpty = lock.newCondition();
void put(E e) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 入队... notEmpty.signal(); } finally { lock.unlock(); }}读写锁与 StampedLock
Section titled “读写锁与 StampedLock”ReentrantReadWriteLock
Section titled “ReentrantReadWriteLock”适用于”读多写少”场景,允许多个读线程并发读,写线程独占。
读锁(共享锁):多线程可同时持有,写时不能持有读锁写锁(排他锁):独占,写时不允许任何读/写读写互斥,写写互斥,读读不互斥
AQS state 字段的巧妙利用: 高 16 位:读锁持有数 低 16 位:写锁重入次数StampedLock(Java 8+)
Section titled “StampedLock(Java 8+)”引入乐观读,解决 RRWL 的写饥饿和读性能问题。
StampedLock sl = new StampedLock();
// 乐观读(不加锁,只获取一个 stamp)long stamp = sl.tryOptimisticRead();double x = this.x;double y = this.y;if (!sl.validate(stamp)) { // 检查读期间是否有写操作 // 乐观读失败,升级为悲观读锁 stamp = sl.readLock(); try { x = this.x; y = this.y; } finally { sl.unlockRead(stamp); }}
// 写锁long stamp = sl.writeLock();try { this.x = newX; this.y = newY;} finally { sl.unlockWrite(stamp);}Condition 与等待/通知机制
Section titled “Condition 与等待/通知机制”每个 Condition 对象内部维护一个 Condition Queue(条件队列): 与 AQS 的 Sync Queue(同步队列)分离
await() 流程: 1. 将当前线程包装为 Node 加入 Condition Queue 2. 释放锁(state 归零) 3. LockSupport.park() 挂起
signal() 流程: 1. 将 Condition Queue 队首 Node 转移到 Sync Queue 2. unpark 该节点线程(让其参与锁竞争) 3. 线程被唤醒后从 await() 处继续,重新竞争锁对比 Object.wait/notify:
Object.wait/notify:与 synchronized 绑定,每个对象只有一个等待集合Condition.await/signal:与 Lock 绑定,一个 Lock 可有多个 Condition,精确唤醒二、面试突击篇
Section titled “二、面试突击篇”2.1 原理分析题
Section titled “2.1 原理分析题”Q1: synchronized 的锁升级过程是怎样的?
🎯 考察重点: synchronized 底层实现原理
📝 回答要点:
-
无锁 → 偏向锁
- 第一个线程访问同步块时,将 Mark Word 设为偏向锁状态
- 记录线程 ID,后续同一线程进入无需任何同步操作
-
偏向锁 → 轻量级锁
- 有其他线程竞争时,撤销偏向锁
- 竞争线程在各自栈帧创建 Lock Record,CAS 替换 Mark Word
- 自旋等待,适合锁持有时间短的场景
-
轻量级锁 → 重量级锁
- 自旋超过阈值或等待线程多时升级
- 操作系统 Mutex,线程进入 BLOCKED 状态
- 涉及用户态↔内核态切换
🔍 追问扩展:
-
Q: 锁可以降级吗? A: 正常情况下只升级不降级。特殊:STW 期间有批量降级机制。
-
Q: 为什么 JDK 15+ 废弃偏向锁? A: 多线程应用中,偏向锁撤销的 STW 开销得不偿失。
2.2 核心机制题
Section titled “2.2 核心机制题”Q2: AQS 的核心原理是什么?
🎯 考察重点: AQS 队列管理、同步状态控制
📝 回答要点:
-
核心组件
state:volatile 同步状态变量- CLH 变体双向队列:等待线程队列
-
工作流程
tryAcquire():子类实现,尝试 CAS 修改 stateaddWaiter():获取失败则加入队列acquireQueued():自旋等待,park/unpark
-
支撑作用
- ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS
🔍 追问扩展:
- Q: AQS 如何实现公平锁?
A:
hasQueuedPredecessors()检查队列是否有等待者,有则入队等待。
2.3 实战应用题
Section titled “2.3 实战应用题”Q3: synchronized 和 ReentrantLock 怎么选?
🎯 考察重点: 锁的特性理解、技术选型能力
📝 回答要点:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 简单同步 | synchronized | 代码简洁,JVM 内置 |
| 需要超时 | ReentrantLock | tryLock(timeout) |
| 需要中断 | ReentrantLock | lockInterruptibly() |
| 需要多个 Condition | ReentrantLock | newCondition() |
| 需要公平锁 | ReentrantLock | new ReentrantLock(true) |
| 读多写少 | StampedLock | 乐观读,高性能 |
💡 记忆技巧: “sync 简单用,RL 高级用,读写 stamped”
3.1 核心要点回顾
Section titled “3.1 核心要点回顾”| 知识点 | 关键点 | 面试权重 |
|---|---|---|
| synchronized 锁升级 | 无锁→偏向→轻量→重量 | ★★★★★ |
| AQS 队列 | CLH 变体、state 管理 | ★★★★★ |
| ReentrantLock | 可重入、tryLock、Condition | ★★★★☆ |
| StampedLock | 乐观读、写饥饿 | ★★★☆☆ |
3.2 学习建议
Section titled “3.2 学习建议”- 掌握锁升级:4 种状态及转换条件要能默写
- 理解 AQS:队列管理 + state 状态控制是核心
- 合理选型:根据场景选择合适的锁
3.3 扩展阅读
Section titled “3.3 扩展阅读”- JDK 源码:
AbstractQueuedSynchronizer.java、ReentrantLock.java - 《Java 并发编程的艺术》第 4-5 章
- JEP 374: Deprecate and Disable Biased Locking