AQS 中的公平锁 vs 非公平锁如何实现?
在 Java 的 AQS(AbstractQueuedSynchronizer)中,公平锁 vs 非公平锁是控制线程获取锁顺序的两种策略。它们分别代表:
- 公平锁(Fair Lock):线程获取锁遵循先来先服务,排队执行。
- 非公平锁(Non-Fair Lock):线程来就尝试抢锁,可能插队,不一定先到先得。
这两种策略影响锁的获取效率、线程调度的公平性,具体由 AQS 与 ReentrantLock 的实现方式决定。
一、快速结论对比表
| 特性 | 公平锁 (fair=true) | 非公平锁 (fair=false) |
|---|---|---|
| 获取顺序 | 严格按照队列 FIFO 顺序 | 可能“插队”,只要锁空闲就抢 |
| 性能表现 | 较差(线程频繁挂起/唤醒) | 更高效(减少上下文切换) |
| 线程饥饿可能 | 几乎没有 | 有可能某线程长期抢不到锁 |
| 实现复杂度 | 较高(需要判断前驱节点) | 简单(直接尝试 CAS) |
| AQS 方法 | 重写 tryAcquire() + hasQueuedPredecessors() 判断 | 直接 CAS 尝试更新 state |
| 应用场景 | 实时性高、对顺序要求强(如任务调度) | 大多数高并发、吞吐量优先的应用场景 |
二、源码视角剖析:如何实现公平/非公平
ReentrantLock 是如何指定公平性的?
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁(默认)
底层源码:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
它内部用两个子类来实现不同策略:
FairSync extends SyncNonfairSync extends Sync
1. 非公平锁:NonfairSync.tryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接尝试获取锁,抢到就成功(不管队列有没有其他线程)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
关键点:没有检查等待队列!锁空就抢,抢到为王。
2. 公平锁:FairSync.tryAcquire()
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;
setState(nextc);
return true;
}
return false;
}
🔍 hasQueuedPredecessors():
public final boolean hasQueuedPredecessors() {
Node h, t, s;
return ((h = head) != (t = tail) &&
((s = h.next) == null || s.thread != Thread.currentThread()));
}
关键点:当前线程只有在**自己是等待队列的头节点(排在第一)**才允许获取锁。
三、举个线程抢锁的例子对比
场景:
三个线程 A、B、C,依次请求锁。
- 公平锁:
A 获得锁 → A 释放 → B 获得 → B 释放 → C 获得
- 非公平锁:
A 获得锁 → A 释放 → C 抢先 → B 被饿死一会
四、为什么非公平锁更快?
非公平锁减少了以下开销:
- 不检查等待队列(少了
hasQueuedPredecessors()) - 避免线程频繁挂起/唤醒(直接尝试)
- 减少线程上下文切换
性能差异测试(高并发场景):
| 锁类型 | 吞吐量(请求数/秒) |
|---|---|
| 非公平锁 | ✅ 更高 |
| 公平锁 | ❌ 明显下降 |
但注意:非公平锁存在线程饥饿风险。
五、官方与源码资料
- OpenJDK ReentrantLock 源码
- OpenJDK AQS 源码
- 《Java 并发编程实战》第 14 章
- 《深入理解 Java 虚拟机》第 13 章(锁优化与公平性)
总结口诀
公平排队,保障顺序;非公平锁,抢占优先;性能优先选非公平,调度严格用公平锁。
更多详细内容请关注其他相关文章!