Java ReentrantLock的公平锁与非公平锁源码对比
核心差异概述
ReentrantLock 是 Java 并发包中最常用的可重入锁。它内部通过一个 Sync 抽象类实现,并提供了两个具体子类:FairSync(公平锁)和 NonfairSync(非公平锁)。两者的核心区别在于线程获取锁的顺序:公平锁按照线程等待的先后顺序(FIFO)分配锁,非公平锁允许线程在尝试获取锁时“插队”。
本文从源码角度逐行对比,帮助你彻底理解两者的实现机制。
1. 从 lock() 方法开始
这是两种锁最直观的入口。
公平锁的 lock()
打开 ReentrantLock.java,找到 FairSync 内部的 lock() 方法:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
调用 acquire(1) 方法。acquire 是 AbstractQueuedSynchronizer(AQS)的模板方法,它内部会依次:
- 尝试获取锁(调用
tryAcquire)。 - 若失败,将当前线程加入等待队列。
非公平锁的 lock()
对应 NonfairSync 的 lock():
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
关键区别:非公平锁在进入 acquire 逻辑前,先直接尝试一次 CAS 抢锁。如果成功,则不进入队列,直接获得锁,即使此时队列中已有等待线程。这就是“插队”的源头。
2. 核心方法 tryAcquire 对比
tryAcquire 是决定公平性的关键。两者都重写了 AQS 的 tryAcquire(int acquires)。
公平锁的 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;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
重点在于 hasQueuedPredecessors() 方法。它的作用是判断当前线程前面是否有其他等待线程。如果有,即使锁是空闲的,当前线程也不能抢锁,必须排队。
非公平锁的 tryAcquire
protected final boolean tryAcquire(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;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
注意:非公平锁的 tryAcquire 中没有调用 hasQueuedPredecessors()。当锁空闲(c == 0)时,它直接尝试 CAS 设置状态,成功即获得锁。这样,即使在等待队列头部有个线程已经等待了很久,新的线程仍可能抢锁成功。
3. hasQueuedPredecessors() 源码解析
这个方法是公平锁公平性的保障。打开 AbstractQueuedSynchronizer 查看:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
逻辑解释:
- 如果队列为空(
h == t),返回false,表示没有前驱,可以抢锁。 - 如果队列不为空,但头节点的下一个节点的线程就是当前线程,也返回
false,表示当前线程就是下一个需要服务的线程,允许抢锁。 - 其他情况(队列中有等待线程,且当前线程不在队列头部)返回
true,表示有前驱,禁止抢锁。
公平锁正是通过这个方法强制所有线程按照入队顺序获取锁。
4. 对比总结表
| 对比维度 | 公平锁(FairSync) | 非公平锁(NonfairSync) |
|---|---|---|
lock() 入口 |
直接调用 acquire(1) |
先尝试 CAS 抢一次锁,失败后调 acquire(1) |
tryAcquire 中是否检查队列前驱 |
是,调用 hasQueuedPredecessors() |
否,直接 CAS |
| 线程获取锁顺序 | FIFO(先来先服务) | 可能插队(允许新线程比老线程先获得锁) |
| 吞吐量表现 | 较公平,但上下文切换较多 | 通常更高,因为减少了线程挂起和唤醒 |
| 适用场景 | 对锁等待时间敏感,避免线程饥饿 | 追求高吞吐量,能容忍短暂的不公平 |
5. 性能与选择建议
非公平锁之所以“默认”被选择(ReentrantLock 的无参构造器默认创建非公平锁),是因为在大多数高并发场景下,允许插队能显著减少线程挂起和恢复的次数。
- 非公平锁:当一个线程释放锁时,正好有另一个新线程请求锁。新线程直接 CAS 成功,无需进入队列,也无需唤醒队列中的老线程。这避免了两个线程进行昂贵的上下文切换。
- 公平锁:每次锁释放都必唤醒队列中第一个等待线程,哪怕此时有新线程请求,也必须乖乖入队。这会增加系统开销。
操作建议:
- 优先使用非公平锁,除非你的业务明确要求锁的公平性(例如,必须按请求顺序执行任务)。
- 如果担心线程饥饿,非公平锁在高竞争下极少数线程可能长时间得不到锁;此时可以改用公平锁,或者控制锁持有的时间。
6. 完整代码结构回顾
ReentrantLock 内部类关系:
ReentrantLock
└── Sync (抽象,继承AQS)
├── NonfairSync (非公平)
└── FairSync (公平)
Sync提供公共方法如nonfairTryAcquire,但NonfairSync重写了tryAcquire,覆盖了父类的实现。- 公平锁的
tryAcquire完全自己实现,不调用父类的nonfairTryAcquire。
关键行为区别(用文字描述两个线程的竞争场景):
场景:锁被线程A持有,线程B和线程C同时等待。线程A释放锁的瞬间,线程D刚好调用 lock()。
- 非公平锁:线程D直接 CAS 尝试获取锁,成功则获得锁,线程B和线程C继续等待。
- 公平锁:线程D发现队列中有等待线程(B或C),调用
hasQueuedPredecessors()返回true,因此即使锁空闲,线程D也只能入队排队,保证B或C先获得锁。

暂无评论,快来抢沙发吧!