在 Java 并发包中,ReentrantLock 的公平锁 FairSync 是理解 AbstractQueuedSynchronizer(简称 AQS)线程唤醒机制的典型入口。AQS 内部维护了一个基于 CLH 队列改良的双向等待队列,通过控制同步状态 state 与节点出队顺序,决定哪个线程可以获得锁。FairSync 在加锁时强制排队,在释放锁时严格按照队列顺序唤醒后继节点,这与非公平锁的抢占式唤醒形成鲜明对比。

一、FairSync 的基本结构与加锁逻辑
FairSync 继承自 Sync,而 Sync 又继承自 AbstractQueuedSynchronizer。它的核心在于重写了 tryAcquire 方法,在尝试获取锁之前先判断队列中是否有前驱节点在等待。如果有,当前线程就必须进入队列排队,而不能直接抢锁。
下面的代码展示了 FairSync 中 tryAcquire 的典型实现逻辑。通过 hasQueuedPredecessors 判断是否需要排队,这是公平性的关键所在。只有队列为空或当前线程就是头节点的后继时,才会尝试用 CAS 修改 state。
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
从这段代码可以看出,FairSync 的加锁策略非常直观:只要前面有人排队,新来的线程就乖乖去队列尾部等待。这种策略保证了先到先得,但也意味着即使是空闲瞬间,线程也要走一遍排队流程,吞吐量略低于非公平模式。
hasQueuedPredecessors 方法会检查头节点是否就是当前线程所在节点,或者队列中是否已有有效等待节点。它的存在让 AQS 的唤醒策略从根源上避免了后来线程插队的可能,为后续释放锁时的顺序唤醒打下基础。
二、AQS 队列中节点的等待与唤醒关系
AQS 的等待队列是一个带有头尾指针的双向链表,每个节点保存了等待线程、等待状态 waitStatus 以及前后指针。当一个线程获取锁失败时,会被包装成节点通过 addWaiter 方法追加到队尾,并通过 LockSupport.park 挂起。
在公平锁场景下,头节点代表当前持有锁的线程(或刚释放锁的虚拟头),真正的第一个等待者位于头节点的 next。释放锁时,AQS 只会唤醒这个 next 节点对应的线程,而不是随机或广播式唤醒,这就是所谓的“队列对等待线程的唤醒策略”。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
上面的 unparkSuccessor 是 AQS 释放锁时唤醒后继的核心方法。它首先清理当前节点状态,然后从 next 开始寻找一个状态合法的节点进行 unpark。在 FairSync 的正常流程中,next 通常就是队列中第一个有效等待线程,因此唤醒具有严格的顺序性。
这种策略的好处是不会出现线程饥饿,每个排队的线程都能按进入队列的顺序被唤醒。缺点是如果头节点释放后 next 节点所在线程被操作系统调度延迟,后续节点也只能干等,系统整体的并发响应会偏保守。
三、从 release 方法看公平唤醒的完整流程
ReentrantLock 的 unlock 方法会调用 Sync 的 release,而 release 又调用 AQS 的模板方法。只有 tryRelease 成功将 state 归零后,才会触发 unparkSuccessor,完成一次公平的唤醒传递。
下面的代码简化展示了 FairSync 释放锁的调用链。注意 tryRelease 并不区分公平或非公平,真正决定唤醒顺序的是 AQS 队列结构和 unparkSuccessor 的取值逻辑。
public void unlock() {
sync.release(1);
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
当 state 变为 0,release 方法会拿头节点调用 unparkSuccessor。此时头节点一般是刚释放锁的线程节点(在 AQS 中释放后头节点会被后继替换),它的 next 就是下一个该被唤醒的公平等待者。线程被唤醒后,会再次执行 tryAcquire,由于此时队列里没有比它更靠前的节点,它能够顺利拿到锁。
通过这段流程可以确认:AQS 对等待线程的唤醒策略在公平模式下表现为严格的先进先出。它不像条件队列那样可能唤醒多个线程,也不像非公平锁那样允许新线程在释放瞬间插队,而是将唤醒对象精确限定为队列首部有效节点。
四、公平唤醒策略的优缺点与适用场景
从 FairSync 源码可以总结出,AQS 的顺序唤醒策略以等待队列为唯一仲裁者,避免了资源分配的不确定性。在需要严格避免线程饥饿、且锁持有时间较短的业务中,公平锁能让调用方获得可预期的延迟表现。
但在高并发且锁竞争激烈的场景里,公平锁由于每次都要排队和唤醒,会导致更多的上下文切换。此时非公平锁的抢占机制往往能提升吞吐量。理解 FairSync 与 AQS 的协作方式,能帮助开发者在写并发组件时正确权衡,也能在排查线程堆积问题时快速定位是否因公平排队引起。
// 使用示例:公平锁的创建与释放
ReentrantLock fairLock = new ReentrantLock(true);
fairLock.lock();
try {
// 临界区操作
} finally {
fairLock.unlock();
}
上面这段示例虽然简单,但背后隐藏的正是 AQS 队列对等待线程的唤醒契约。只要在代码中始终使用 try-finally 保证 unlock 执行,FairSync 就能依靠 AQS 的节点唤醒规则,把锁从当前线程平稳交给队列中的下一位等待者。
总的来说,借助 ReentrantLock 的 FairSync 源码,我们能够把 AQS 中抽象的队列与状态机制映射到具体的方法调用上。看清 tryAcquire 的排队判断与 unparkSuccessor 的定点唤醒,也就真正明白了 AQS 是如何管理等待线程的生命周期与执行顺序的。
ReentrantLockFairSyncAbstractQueuedSynchronizer修改时间:2026-08-08 02:12:36