Skip to content

锁机制与 AQS - 并发 | 炼金塔


  • 面试必考核心:synchronized 锁升级是 Java 并发最高频考点
  • 性能优化关键:理解锁的代价才能进行合理优化
  • 死锁排查基础:掌握锁机制才能分析并发问题
场景痛点解决方案
库存扣减超卖问题synchronized / 分布式锁
账户余额并发更新丢失乐观锁 / CAS
缓存更新读多写少读写锁
订单状态状态机流转公平锁

synchronized 是 Java 内置的互斥锁,基于 JVM 的 Monitor(监视器) 机制实现。

每个 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 内部的 synchronized
String 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 是 J.U.C 锁和同步工具的核心框架ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等都基于它实现。

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) 唤醒后继节点
// 非公平锁(默认):新来的线程先 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 的线程大概率能直接再次获取锁,提高了吞吐量。代价是可能导致某些线程长时间等待(饥饿)。


对比项synchronizedReentrantLock
实现层级JVM 内置,字节码层面JDK 类库,Java 代码实现
锁释放自动(退出同步块)必须手动 unlock(),需用 try-finally
可重入支持支持
公平锁不支持支持(new ReentrantLock(true)
尝试加锁不支持tryLock() / tryLock(time, unit)
可中断等待不支持lockInterruptibly()
条件变量单一 wait/notify多个 Conditionawait()/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(); }
}

适用于”读多写少”场景,允许多个读线程并发读,写线程独占。

读锁(共享锁):多线程可同时持有,写时不能持有读锁
写锁(排他锁):独占,写时不允许任何读/写
读写互斥,写写互斥,读读不互斥
AQS state 字段的巧妙利用:
高 16 位:读锁持有数
低 16 位:写锁重入次数

引入乐观读,解决 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 对象内部维护一个 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,精确唤醒

Q1: synchronized 的锁升级过程是怎样的?

🎯 考察重点: synchronized 底层实现原理

📝 回答要点:

  1. 无锁 → 偏向锁

    • 第一个线程访问同步块时,将 Mark Word 设为偏向锁状态
    • 记录线程 ID,后续同一线程进入无需任何同步操作
  2. 偏向锁 → 轻量级锁

    • 有其他线程竞争时,撤销偏向锁
    • 竞争线程在各自栈帧创建 Lock Record,CAS 替换 Mark Word
    • 自旋等待,适合锁持有时间短的场景
  3. 轻量级锁 → 重量级锁

    • 自旋超过阈值或等待线程多时升级
    • 操作系统 Mutex,线程进入 BLOCKED 状态
    • 涉及用户态↔内核态切换

🔍 追问扩展:

  • Q: 锁可以降级吗? A: 正常情况下只升级不降级。特殊:STW 期间有批量降级机制。

  • Q: 为什么 JDK 15+ 废弃偏向锁? A: 多线程应用中,偏向锁撤销的 STW 开销得不偿失。


Q2: AQS 的核心原理是什么?

🎯 考察重点: AQS 队列管理、同步状态控制

📝 回答要点:

  1. 核心组件

    • state:volatile 同步状态变量
    • CLH 变体双向队列:等待线程队列
  2. 工作流程

    • tryAcquire():子类实现,尝试 CAS 修改 state
    • addWaiter():获取失败则加入队列
    • acquireQueued():自旋等待,park/unpark
  3. 支撑作用

    • ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS

🔍 追问扩展:

  • Q: AQS 如何实现公平锁? A: hasQueuedPredecessors() 检查队列是否有等待者,有则入队等待。

Q3: synchronized 和 ReentrantLock 怎么选?

🎯 考察重点: 锁的特性理解、技术选型能力

📝 回答要点:

场景推荐原因
简单同步synchronized代码简洁,JVM 内置
需要超时ReentrantLocktryLock(timeout)
需要中断ReentrantLocklockInterruptibly()
需要多个 ConditionReentrantLocknewCondition()
需要公平锁ReentrantLocknew ReentrantLock(true)
读多写少StampedLock乐观读,高性能

💡 记忆技巧: “sync 简单用,RL 高级用,读写 stamped”


知识点关键点面试权重
synchronized 锁升级无锁→偏向→轻量→重量★★★★★
AQS 队列CLH 变体、state 管理★★★★★
ReentrantLock可重入、tryLock、Condition★★★★☆
StampedLock乐观读、写饥饿★★★☆☆
  1. 掌握锁升级:4 种状态及转换条件要能默写
  2. 理解 AQS:队列管理 + state 状态控制是核心
  3. 合理选型:根据场景选择合适的锁
  • JDK 源码:AbstractQueuedSynchronizer.javaReentrantLock.java
  • 《Java 并发编程的艺术》第 4-5 章
  • JEP 374: Deprecate and Disable Biased Locking