Java ReentrantLock的公平锁与非公平锁性能差异实测
Java并发包中的ReentrantLock提供了两种锁获取模式:公平锁与非公平锁。了解两者的性能差异对于编写高并发程序至关重要。虽然公平锁听起来更符合直觉,但在实际的高性能场景中,非公平锁往往才是默认且更优的选择。
1. 理解核心机制差异
在深入代码之前,先从逻辑层面区分两者的工作方式。这种差异决定了它们在极端并发下的表现。
- 公平锁:严格按照线程请求锁的先后顺序来分配锁。线程在获取锁之前,会先检查是否有其他线程在排队。如果有,自己乖乖排到队尾。
- 非公平锁:允许“插队”。线程在请求锁时,不管队列里是否有其他线程在等待,都直接尝试抢锁。如果抢不到,再去排队。
为了更直观地展示非公平锁的“插队”过程,通过以下流程图查看线程获取锁的逻辑分支:
从流程中可以看出,非公平锁在G节点多了一次直接抢锁的机会,这正是其性能优势的来源。
2. 深入源码实现
通过对比JDK中ReentrantLock的两种实现类FairSync和NonfairSync的源码,可以精确找到性能差异的根源。
非公平锁实现
非公平锁在调用lock()方法时,非常激进,第一步就是尝试修改状态(CAS操作)。
static final class NonfairSync extends Sync {
final void lock() {
// 直接尝试抢占锁,无视等待队列
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
公平锁实现
公平锁在tryAcquire中增加了一个关键判断:hasQueuedPredecessors()。
static final class FairSync extends Sync {
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;
}
}
// ...省略重入逻辑
return false;
}
}
分析差异:
非公平锁在lock()入口处直接执行compareAndSetState,只要锁是空闲的,当前线程就能直接获取,完全跳过了检查队列的逻辑。而公平锁每次都要调用hasQueuedPredecessors(),这需要遍历或检查AQS队列的头节点,增加了额外的开销。
3. 性能差异实测与分析
在实际测试中(例如多线程循环递增计数器),非公平锁的性能通常远高于公平锁。
实测数据对比
基于标准的多核CPU环境(如4核),对5000个线程进行并发争用测试,结果对比如下:
| 锁类型 | 执行顺序 | 吞吐量表现 | 线程唤醒成本 |
|---|---|---|---|
| 非公平锁 | 无序,可能插队 | 高(基准) | 低 |
| 公平锁 | 严格FIFO | 低(约为非公平锁的1/5到1/10) | 高 |
根据性能测试数据,非公平锁的吞吐量通常是公平锁的 5~10倍。
性能差异的数学逻辑
在并发环境下,线程挂起(挂起是指线程从运行态切换到阻塞态)和唤醒(唤醒是指线程从阻塞态切换回运行态)涉及操作系统内核态的切换,这是一个极其昂贵的操作。
假设:
- $T_{switch}$ 为一次线程上下文切换的时间。
- $T_{cas}$ 为一次CAS原子操作的时间。
- $N$ 为线程争抢次数。
对于非公平锁,当一个线程释放锁时,刚好有一个新线程到来尝试获取:
- 如果新线程CAS成功,它直接获取锁执行,避免了 $T_{switch}$。
对于公平锁:
- 即使锁刚好释放,它也必须唤醒队列头部的等待线程,必然发生 $T_{switch}$。
因此,在大量竞争场景下,非公平锁节省的时间总和 $\Delta T$ 可以近似表示为:
$$ \Delta T \approx N_{bypass} \times T_{switch} $$
其中 $N_{bypass}$ 是成功插队的次数。由于 $T_{switch} \gg T_{cas}$,这解释了为什么非公平锁在“读多写少”或一般并发场景下具有压倒性的性能优势。
4. 实战应用指南
既然非公平锁性能如此优异,为什么还要保留公平锁?这取决于业务场景对顺序的敏感度。
使用非公平锁(默认)
在绝大多数业务场景下,使用非公平锁。ReentrantLock和synchronized默认都是非公平的。
- 适用场景:Web服务器后台处理、数据库连接池获取、通用缓存更新。
- 理由:吞吐量优先,线程获取锁的短暂顺序差异不影响业务数据的最终一致性。
使用公平锁
仅在明确需要严格顺序时使用公平锁。通过构造函数new ReentrantLock(true)开启。
- 适用场景:银行账户转账排队、严格的任务队列处理、需要防止某个线程因为频繁“插队”导致其他线程“饥饿”的场景。
- 理由:保证先来后到,避免线程长时间抢不到锁(饥饿现象),但需要承受显著的性能下降。
5. 总结代码示例
以下代码演示如何在代码中根据需求选择锁类型。
import java.util.concurrent.locks.ReentrantLock;
public class LockSelection {
// 创建非公平锁(默认)
private final ReentrantLock unfairLock = new ReentrantLock();
// 创建公平锁
private final ReentrantLock fairLock = new ReentrantLock(true);
public void performTaskWithUnfairLock() {
unfairLock.lock();
try {
// 高风险操作:高并发执行,不保证顺序,但速度最快
System.out.println(Thread.currentThread().getName() + " is running (Unfair)");
} finally {
unfairLock.unlock();
}
}
public void performTaskWithFairLock() {
fairLock.lock();
try {
// 顺序敏感操作:严格按照请求顺序执行,速度较慢
System.out.println(Thread.currentThread().getName() + " is running (Fair)");
} finally {
fairLock.unlock();
}
}
}
在实际开发中,除非有明确的顺序性需求,否则坚持使用默认的非公平锁以获得最佳的系统吞吐量。

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