Lock 接口与 AQS
问题
AQS(AbstractQueuedSynchronizer)的核心原理是什么?ReentrantLock 如何基于 AQS 实现加锁和解锁?公平锁和非公平锁有什么区别?
答案
Lock 接口
java.util.concurrent.locks.Lock 是 JDK 5 引入的锁接口,比 synchronized 提供更灵活的锁操作:
public interface Lock {
void lock(); // 获取锁(阻塞)
void lockInterruptibly() throws InterruptedException; // 可中断获取
boolean tryLock(); // 尝试获取(非阻塞)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
Lock 不像 synchronized 那样自动释放,必须手动在 finally 中调用 unlock(),否则可能导致死锁:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 确保锁一定释放
}
AQS 核心原理
AQS(AbstractQueuedSynchronizer) 是 JUC 包中大部分同步组件的基础框架,包括 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都基于 AQS 实现。
核心结构
AQS 维护两个关键数据:
// 同步状态
private volatile int state;
// CLH 变体双向队列的头尾指针
private transient volatile Node head;
private transient volatile Node tail;
state 的含义
state 的具体含义由子类定义:
| 同步组件 | state 含义 |
|---|---|
| ReentrantLock | 0 = 未锁定,≥ 1 = 锁定(值为重入次数) |
| Semaphore | 可用许可数 |
| CountDownLatch | 剩余计数 |
| ReentrantReadWriteLock | 高 16 位 = 读锁数,低 16 位 = 写锁重入数 |
CLH 队列(等待队列)
获取锁失败的线程会被封装成 Node 节点加入 CLH 双向队列并阻塞:
static final class Node {
volatile int waitStatus; // 节点状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // Condition 队列中的下一个节点
}
Node 的 waitStatus:
| 状态 | 值 | 说明 |
|---|---|---|
| CANCELLED | 1 | 线程已取消 |
| SIGNAL | -1 | 后继节点需要被唤醒 |
| CONDITION | -2 | 在 Condition 队列中等待 |
| PROPAGATE | -3 | 共享模式下传播唤醒 |
| 0 | 0 | 初始状态 |
AQS 获取锁流程(以独占模式为例)
// 模板方法
public final void acquire(int arg) {
// 1. 尝试获取锁(由子类实现)
if (!tryAcquire(arg) &&
// 2. 获取失败,加入等待队列并阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS 释放锁流程
public final boolean release(int arg) {
// 1. 尝试释放锁(由子类实现)
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 2. 唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantLock 基于 AQS 的实现
非公平锁(默认)
// 非公平锁的 tryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// state == 0 表示锁空闲,直接 CAS 竞争(不管队列中是否有等待线程)
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 当前线程已持有锁 → 重入,state + 1
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 获取失败,进入 AQS 队列
}
公平锁
// 公平锁的 tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 关键区别:先检查队列中是否有等待更久的线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
非公平锁:新线程到来时直接 CAS 抢锁,失败才排队。吞吐量高,但可能饥饿。
公平锁:新线程到来时先检查队列,有等待线程则乖乖排队。严格 FIFO,但性能略差(多一次 hasQueuedPredecessors() 判断 + 更多上下文切换)。
Condition 条件变量
Condition 是 Lock 版的 wait/notify,一个 Lock 可以创建多个 Condition:
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 条件:不为空
Condition notFull = lock.newCondition(); // 条件:不为满
// 生产者
lock.lock();
try {
while (queue.isFull()) {
notFull.await(); // 等待"不满"条件
}
queue.add(item);
notEmpty.signal(); // 通知"不为空"
} finally {
lock.unlock();
}
// 消费者
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待"不为空"条件
}
Item item = queue.poll();
notFull.signal(); // 通知"不满"
} finally {
lock.unlock();
}
Condition 内部维护一个条件队列(单向链表),await() 时线程从 AQS 同步队列转移到条件队列,signal() 时从条件队列转移回 AQS 同步队列。
独占模式 vs 共享模式
AQS 支持两种模式:
| 模式 | 方法 | 代表实现 |
|---|---|---|
| 独占 | tryAcquire / tryRelease | ReentrantLock |
| 共享 | tryAcquireShared / tryReleaseShared | Semaphore、CountDownLatch、ReadWriteLock(读锁) |
共享模式下,一个线程获取锁成功后,如果后续节点也是共享模式,会传播唤醒,让多个线程同时获取。
常见面试问题
Q1: AQS 的核心原理是什么?
答案:
AQS 的核心是一个 volatile int state(同步状态)和一个 CLH 变体双向等待队列。
- 获取锁:尝试通过 CAS 修改 state,成功则获取锁,失败则封装为 Node 加入队列并阻塞(
LockSupport.park()) - 释放锁:修改 state,唤醒队列中下一个等待线程(
LockSupport.unpark()) - AQS 使用模板方法模式,
acquire/release是模板方法,tryAcquire/tryRelease由子类实现
Q2: 公平锁和非公平锁的区别?为什么默认非公平?
答案:
| 对比 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取策略 | 先检查队列,有等待线程则排队 | 直接 CAS 抢锁,失败才排队 |
| 线程饥饿 | 不会 | 可能 |
| 吞吐量 | 较低 | 较高 |
| 上下文切换 | 更多 | 更少 |
默认非公平的原因:非公平锁允许新线程直接插队,如果刚好锁空闲就能立即执行,减少了线程唤醒和切换的开销。实测非公平锁的吞吐量可以高出数倍。
Q3: ReentrantLock 的可重入是如何实现的?
答案:
在 tryAcquire() 中,如果当前线程已经是锁的持有者(getExclusiveOwnerThread() == current),则将 state 加 1。释放时 state 减 1,减到 0 才真正释放锁。
Q4: AQS 中为什么用 CLH 队列而不是普通队列?
答案:
CLH(Craig, Landin, Hagersten)锁的优点:
- 入队 CAS 操作简单:只需 CAS 修改 tail 指针
- 公平性:天然 FIFO 顺序
- 取消方便:标记节点状态即可,不需要实际删除
AQS 使用的是 CLH 的变体——双向链表,相比原版 CLH 增加了 prev 指针,方便取消和释放时的前驱查找。
Q5: Condition 和 Object.wait/notify 的区别?
答案:
| 对比 | Object wait/notify | Condition await/signal |
|---|---|---|
| 配合的锁 | synchronized | Lock |
| 条件队列个数 | 1 个 | 多个(每个 Condition 一个) |
| 释放锁 | wait() 释放 | await() 释放 |
| 唤醒 | notify()/notifyAll() | signal()/signalAll() |
| 精准唤醒 | 不支持 | 支持(不同 Condition 唤醒不同线程) |
| 超时/中断 | 有限支持 | 更完善(awaitNanos 等) |
Q6: 哪些 JUC 组件基于 AQS?
答案:
- ReentrantLock:独占模式,state 表示重入次数
- ReentrantReadWriteLock:读锁共享模式,写锁独占模式
- Semaphore:共享模式,state 表示许可数
- CountDownLatch:共享模式,state 表示计数
- CyclicBarrier:内部使用 ReentrantLock + Condition
Q7: 为什么 AQS 的 state 用 volatile 修饰?
答案:
volatile 保证了 state 的可见性和有序性(详见 Java 内存模型)。当一个线程修改 state 后,其他线程能立即看到最新值。配合 CAS 操作(compareAndSetState()),实现了无锁的原子状态更新。
相关链接
- AbstractQueuedSynchronizer - Java 17 API
- ReentrantLock - Java 17 API
- synchronized 关键字 - synchronized 原理与对比
- CAS 与原子类 - CAS 底层原理
- Java 内存模型(JMM) - volatile 与内存屏障
- 阻塞队列 - 基于 Condition 实现的生产者-消费者