文章目录
-
- [先说结论:公平 vs 非公平](#先说结论:公平 vs 非公平)
- 非公平锁:先抢再说,抢不到再排队
- 公平锁:先看队列,有人排队就别插队
- 一行代码的差异:hasQueuedPredecessors
- 为什么默认是非公平锁?
- 公平锁与非公平锁全景
- 回答技巧与点评
你知道 new ReentrantLock(true) 和 new ReentrantLock(false) 的区别吗?一个参数,天壤之别。一个是"先来先得",一个是"谁能抢到归谁"。面试官最爱问的就是:公平锁和非公平锁底层到底怎么实现的?为什么默认是非公平锁?
今天咱们就把这个设计拆开,看看里面藏着什么智慧。
先说结论:公平 vs 非公平
| 维度 | 公平锁(FairSync) | 非公平锁(NonfairSync) |
|---|---|---|
| 构造参数 | new ReentrantLock(true) | new ReentrantLock(false)(默认) |
| 加锁策略 | 先检查队列,队列为空才 CAS | 直接 CAS 抢锁,不管队列 |
| 吞吐量 | 较低(线程切换频繁) | 较高(减少线程切换) |
| 饥饿问题 | 不会饥饿 | 可能饥饿(等待线程可能一直抢不到) |
| 实现差异 | tryAcquire 多了 hasQueuedPredecessors 检查 | tryAcquire 直接 CAS |
一句话记住:公平锁像排队取号,非公平锁像抢红包------排队讲秩序,抢红包讲手速。
非公平锁:先抢再说,抢不到再排队
非公平锁的加锁流程特别"粗暴":
java
// NonfairSync.lock()
final void lock() {
if (compareAndSetState(0, 1)) // 第1步:直接 CAS 抢锁 👈
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 第2步:抢不到再走 AQS 排队
}
不看队列,不管有没有人在等,上来就抢。 抢到了直接用,抢不到再老老实实排队。
更关键的是,在 tryAcquire 里也直接抢:
java
// NonfairSync.tryAcquire(调用 nonfairTryAcquire)
final boolean nonfairTryAcquire(int acquires) {
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接 CAS,不检查队列 👈
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(c + acquires); // 可重入
return true;
}
return false;
}
生活类比: 你去银行办业务,非公平锁就是------不管大厅里有没有人在等号,你冲到柜台就看能不能办。柜台空着就办了,排队的人?他们继续等。
公平锁:先看队列,有人排队就别插队
公平锁的加锁流程多了一步检查:
java
// FairSync.lock()
final void lock() {
acquire(1); // 没有直接 CAS 抢锁!直接进 AQS 流程 👈
}
// FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 关键!检查队列有没有人等 👈
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if (getExclusiveOwnerThread() == Thread.currentThread()) {
setState(c + acquires);
return true;
}
return false;
}
hasQueuedPredecessors() 是公平锁的灵魂。它检查 CLH 队列中是否还有比当前线程更早等待的节点。如果有,即使 state == 0,也放弃抢锁,乖乖去排队。
生活类比: 还是银行,公平锁就是------你到柜台前,先看大厅有没有人在等号。有人在等,你就去取号排队;没人等,你直接办。先来先得,绝不插队。
一行代码的差异:hasQueuedPredecessors
公平锁和非公平锁的核心差异,就在 tryAcquire 里的这一行:
java
// 公平锁:多了这个检查
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires))
// 非公平锁:没有这个检查
if (compareAndSetState(0, acquires))
hasQueuedPredecessors() 的实现:
java
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
// head != tail → 队列不空
// h.next.thread != 当前线程 → 队首等的人不是我 👈
就这一行检查,决定了"能不能插队"。
为什么默认是非公平锁?
这是一个经典的设计权衡:
非公平锁吞吐量更高。 原因是:当锁释放时,唤醒队列中的线程需要时间(线程从 park 恢复到运行)。在这段"窗口期"内,如果新线程直接 CAS 抢到了锁,就省去了线程切换的开销。
公平锁保证不饥饿。 但代价是每次释放锁都要等队列中的线程恢复,线程切换频繁,整体吞吐量下降。
实测数据:在中等竞争场景下,非公平锁的吞吐量比公平锁高 数十倍 。所以 ReentrantLock 默认用非公平锁------性能优先,大多数场景不需要严格公平。
公平锁与非公平锁全景
公平锁与非公平锁 全景
核心差异
├── 非公平锁(NonfairSync)
│ ├── lock() 直接 CAS 抢锁
│ ├── tryAcquire 不检查队列
│ └── 可能"插队"导致饥饿
└── 公平锁(FairSync)
├── lock() 直接进 AQS 排队
├── tryAcquire 检查 hasQueuedPredecessors
└── 严格 FIFO,不会饥饿
hasQueuedPredecessors
├── head != tail → 队列不空
└── h.next.thread != 当前线程 → 不是队首 → 排队去
选择建议
├── 默认非公平(性能优先)
├── 需要严格 FIFO → 公平锁
└── 饥饿敏感场景 → 公平锁
口诀:非公平直接抢,公平先看队列长,
hasQueuedPredecessors 是关键,吞吐公平要权衡。
回答技巧与点评
标准回答
ReentrantLock 的公平锁和非公平锁通过两个内部类 FairSync 和 NonfairSync 实现。核心差异在 tryAcquire 方法:公平锁在 CAS 抢锁前会调用 hasQueuedPredecessors() 检查 CLH 队列是否有更早等待的线程,有则放弃抢锁去排队;非公平锁直接 CAS 抢锁,不检查队列。默认使用非公平锁,因为非公平锁在锁释放的"窗口期"允许新线程直接获取锁,减少线程切换开销,吞吐量更高。
加分回答
- 设计哲学:公平锁体现了"正义优先"------先来先得,但代价是性能;非公平锁体现了"效率优先"------整体吞吐更高,但可能牺牲个别线程的等待时间。这是典型的公平性与效率的权衡
- 边界情况:非公平锁在极端情况下可能导致"饥饿"------某线程反复被插队,长时间获取不到锁。但在实际应用中极少发生,因为线程调度本身就有随机性。如果业务确实要求严格有序,才需要公平锁
- 实际应用:ZooKeeper 的 Leader 选举用公平锁保证顺序;Kafka 的消费组再均衡也考虑公平性。而大多数 Web 服务器、数据库连接池用非公平锁,追求高吞吐
面试官点评
这道题考的是你对锁设计的权衡思维和源码级理解 。只说"公平锁排队、非公平锁插队"太表面。能讲出 hasQueuedPredecessors 这个关键方法、非公平锁吞吐量更高的原因(线程唤醒窗口期),以及为什么不默认公平锁,才是面试官想听到的。如果你能延伸到实际框架中的选择,说明你有工程视角。
内容有帮助?点赞、收藏、关注三连!评论区等你 💪