跳到主要内容

Lock 接口与 AQS

问题

AQS(AbstractQueuedSynchronizer)的核心原理是什么?ReentrantLock 如何基于 AQS 实现加锁和解锁?公平锁和非公平锁有什么区别?

答案

Lock 接口

java.util.concurrent.locks.Lock 是 JDK 5 引入的锁接口,比 synchronized 提供更灵活的锁操作:

LockInterface.java
public interface Lock {
void lock(); // 获取锁(阻塞)
void lockInterruptibly() throws InterruptedException; // 可中断获取
boolean tryLock(); // 尝试获取(非阻塞)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时获取
void unlock(); // 释放锁
Condition newCondition(); // 创建条件变量
}
必须在 finally 中释放锁

Lock 不像 synchronized 那样自动释放,必须手动在 finally 中调用 unlock(),否则可能导致死锁:

Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 确保锁一定释放
}

AQS 核心原理

AQS(AbstractQueuedSynchronizer) 是 JUC 包中大部分同步组件的基础框架,包括 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等都基于 AQS 实现。

核心结构

AQS 维护两个关键数据:

AQS 核心字段
// 同步状态
private volatile int state;

// CLH 变体双向队列的头尾指针
private transient volatile Node head;
private transient volatile Node tail;

state 的含义

state 的具体含义由子类定义:

同步组件state 含义
ReentrantLock0 = 未锁定,≥ 1 = 锁定(值为重入次数)
Semaphore可用许可数
CountDownLatch剩余计数
ReentrantReadWriteLock高 16 位 = 读锁数,低 16 位 = 写锁重入数

CLH 队列(等待队列)

获取锁失败的线程会被封装成 Node 节点加入 CLH 双向队列并阻塞:

Node 关键字段
static final class Node {
volatile int waitStatus; // 节点状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // Condition 队列中的下一个节点
}

Node 的 waitStatus

状态说明
CANCELLED1线程已取消
SIGNAL-1后继节点需要被唤醒
CONDITION-2在 Condition 队列中等待
PROPAGATE-3共享模式下传播唤醒
00初始状态

AQS 获取锁流程(以独占模式为例)

AQS 获取锁(简化)
// 模板方法
public final void acquire(int arg) {
// 1. 尝试获取锁(由子类实现)
if (!tryAcquire(arg) &&
// 2. 获取失败,加入等待队列并阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

AQS 释放锁流程

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 的实现

非公平锁(默认)

NonfairSync 核心逻辑
// 非公平锁的 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 队列
}

公平锁

FairSync 核心逻辑
// 公平锁的 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;
}
公平 vs 非公平

非公平锁:新线程到来时直接 CAS 抢锁,失败才排队。吞吐量高,但可能饥饿。

公平锁:新线程到来时先检查队列,有等待线程则乖乖排队。严格 FIFO,但性能略差(多一次 hasQueuedPredecessors() 判断 + 更多上下文切换)。

Condition 条件变量

ConditionLock 版的 wait/notify,一个 Lock 可以创建多个 Condition:

ConditionExample.java
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 / tryReleaseReentrantLock
共享tryAcquireShared / tryReleaseSharedSemaphore、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)锁的优点:

  1. 入队 CAS 操作简单:只需 CAS 修改 tail 指针
  2. 公平性:天然 FIFO 顺序
  3. 取消方便:标记节点状态即可,不需要实际删除

AQS 使用的是 CLH 的变体——双向链表,相比原版 CLH 增加了 prev 指针,方便取消和释放时的前驱查找。

Q5: Condition 和 Object.wait/notify 的区别?

答案

对比Object wait/notifyCondition await/signal
配合的锁synchronizedLock
条件队列个数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()),实现了无锁的原子状态更新。

相关链接