公平锁与非公平锁
一、概念定义
公平锁(Fair Lock)
公平锁指多个线程按照请求锁的顺序(FIFO)依次获取锁。先到先得,排队等待。
ReentrantLock fairLock = new ReentrantLock(true); // true = 公平策略非公平锁(Unfair Lock / Nonfair Lock)
非公平锁允许线程插队——新到达的线程会先尝试抢占锁一次,抢占失败才进入等待队列。
ReentrantLock unfairLock = new ReentrantLock(false); // false = 非公平(默认)
// 等价于:
ReentrantLock defaultLock = new ReentrantLock(); // 默认是非公平锁核心对比
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 线程调度 | FIFO 排队,严格按请求顺序 | 允许插队,可能”后发先至” |
| 吞吐量 | 低(上下文切换频繁) | 高(默认选择,性能更优) |
| 公平性 | ✅ 保证顺序 | ❌ 可能导致线程饥饿(极低概率) |
| 适用场景 | 对响应时间敏感、需要公平调度 | 大多数生产环境 |
| 默认策略 | ReentrantLock(false)、synchronized | 两者默认均为非公平 |
默认锁: 无论是
synchronized还是ReentrantLock,默认都是非公平锁(性能优于公平锁)。
二、底层实现原理
2.1 AQS(AbstractQueuedSynchronizer)与等待队列
公平锁和非公平锁的实现基础是 AQS(AbstractQueuedSynchronizer),它内部维护了一个 CLH 变体双向队列(FIFO):
AQS 内部结构:
┌─────────────────────────────────────────────────────────────┐
│ AbstractQueuedSynchronizer │
│ ┌──────────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ state │ │ Node │←→│ Node │←→│ Node │←→│ Node │ │
│ │ (锁状态) │ │(head)│ │ │ │ │ │(tail)│ │
│ └──────────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ ┌──────────┐ │
│ │ exclusive│ ← 当前持有锁的线程 │
│ │OwnerThread│ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 公平锁加锁流程 FairSync#tryAcquire
公平锁的关键在于 hasQueuedPredecessors() 判断——如果有线程在队列中等待更久,当前线程就不能抢锁:
// ReentrantLock.FairSync 源码核心(JDK 17)
static final class FairSync extends Sync {
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁未被持有
// ★ 关键:检查等待队列中是否有前驱节点
if (!hasQueuedPredecessors() && // 没有等待更久的线程
compareAndSetState(0, acquires)) { // CAS 尝试获取
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() 的实现:
// hasQueuedPredecessors: 判断是否有线程比当前线程等待更久
public final boolean hasQueuedPredecessors() {
Node h = head;
Node t = tail;
Node s;
// 队列不为空 且 头节点的后继节点不是当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}2.3 非公平锁加锁流程 NonfairSync#tryAcquire
非公平锁在锁空闲时直接尝试 CAS 抢锁,不会检查等待队列:
// ReentrantLock.NonfairSync 源码核心(JDK 17)
static final class NonfairSync extends Sync {
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 父类 Sync 中的实现
abstract static class Sync extends AbstractQueuedSynchronizer {
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁未被持有
// ★ 直接 CAS 抢锁,不检查等待队列!
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;
}
}2.4 加锁流程对比图
公平锁(FairSync):
线程请求锁
│
▼
┌──────────────────────┐
│ 锁状态检查 │
│ state == 0 ? │
└───────┬──────────────┘
│
┌────┴────┐
│ state=0 │ ← 锁空闲
└────┬────┘
│
▼
┌──────────────────────┐
│ hasQueuedPredecessors│ ← ★ 检查等待队列
│ = false? │ ─── 有前驱 → 排队去
└───────┬──────────────┘
│ true(无前驱)
▼
┌──────────────────────┐
│ CAS 抢锁 │
└──────────────────────┘
非公平锁(NonfairSync):
线程请求锁
│
▼
┌──────────────────────┐
│ 锁状态检查 │
│ state == 0 ? │
└───────┬──────────────┘
│
┌────┴────┐
│ state=0 │ ← 锁空闲
└────┬────┘
│
▼
┌──────────────────────┐
│ 直接 CAS 抢锁 │ ← ★ 不检查队列,直接抢
│ compareAndSetState │ ─── 失败 → 才进入等待队列
└──────────────────────┘
非公平锁的"插队"过程:
时间轴: T1 T2 T3 T4 T5
状态: 持有锁 排 排 排 ← T5 是新来的线程
队 队 队
等 等 等
待 待 待
T5 持有锁的线程释放锁瞬间:
┌ 排队线程(T2/T3/T4)需要从等待队列被唤醒(上下文切换)
└ T5(新线程)直接 CAS 抢锁 → 成功 → 插队成功!
★ 核心差异:
非公平锁给了新线程一次"插队"机会,失败才排队。
这个过程减少了线程挂起/唤醒的开销,提升吞吐量。
三、性能对比分析
3.1 为什么非公平锁性能更好?
非公平锁的优势来源于减少了线程唤醒的开销:
- 线程唤醒成本高:等待队列中的线程被唤醒需要操作系统进行上下文切换(内核态)
- 插队减少切换:新线程通过 CAS 直接抢到锁,避免了排队线程的唤醒开销
- 可接受的不公平:插队成功概率适中,长期来看不至于导致线程饥饿(JVM 实际测试验证)
非公平锁的"插队"带来的性能优势:
时间线:
Thread A 释放锁
│
├── 排队线程 Thread B:被操作系统唤醒(慢,几微秒)
└── 新线程 Thread C:CAS 直接抢锁(快,纳秒级)
↓
Thread C 插队成功
↓
当 Thread C 释放锁时:
Thread B 刚好被唤醒 → 拿到锁
Thread C 已经做完工作
→ 整体吞吐量提升!
3.2 公平锁 vs 非公平锁性能对比
| 指标 | 公平锁 | 非公平锁 | 差距 |
|---|---|---|---|
| 吞吐量 | 低 | 高 | 非公平锁通常高出 1~2 个数量级 |
| 上下文切换频率 | 高 | 低 | 公平锁切换次数多得多 |
| 线程饥饿风险 | 无 | 极低(实际几乎不会发生) | |
| 响应时间波动 | 稳定 | 有波动(可能有线程被连续插队) |
四、synchronized 的公平性
synchronized 作为 Java 内置锁,默认且只能是非公平的:
// synchronized 无法指定公平策略,永远是非公平锁
public synchronized void method() {
// 不能设置为公平
}synchronized 的锁升级机制与公平性:
| 锁状态 | 公平性表现 | 说明 |
|---|---|---|
| 偏向锁 | 不涉及竞争 | 单线程访问,无公平性概念 |
| 轻量级锁 | 非公平 | 自旋+CAS,谁先抢到谁执行 |
| 重量级锁 | 非公平 | ObjectMonitor 的 _EntryList 本身不是严格 FIFO |
虽然重量级锁底层使用队列,但本质上仍是非公平的——因为锁释放时,ObjectMonitor 的实现并不保证严格按入队顺序唤醒。
五、使用场景与选型建议
优先使用非公平锁的场景
- ✅ 大多数生产环境——默认选择,性能更优
- ✅ 对吞吐量要求高的场景
- ✅ 锁持有时间短、竞争不激烈的场景
- ✅ 可以接受短时间的线程调度不公平
使用公平锁的场景
- ❌ 绝大多数情况下不需要
- 真正需要公平调度的场景极其少见
- ⚠️ 注意:公平锁保证的是获取锁的顺序,而非线程执行的总顺序
- 如果业务逻辑对线程执行顺序有严格依赖,应考虑其他更合适的机制(如
Semaphore、CompletableFuture)
// 公平锁使用示例(如果你真的需要)
private final ReentrantLock fairLock = new ReentrantLock(true);
public void execute() {
fairLock.lock();
try {
// 任务处理
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
fairLock.unlock();
}
}选型决策树
需要锁吗?
│
├── 简单同步场景 → synchronized(非公平,简洁)
│
├── 需要高级功能(超时/可中断/多条件)→ ReentrantLock
│ │
│ ├── 默认:非公平锁 new ReentrantLock() ✅ 推荐
│ │
│ └── 特殊情况:公平锁 new ReentrantLock(true)
│ ├── 极其罕见的公平性要求
│ └── 能接受性能下降
│
└── 读多写少场景 → ReentrantReadWriteLock(默认非公平)
六、总结
公平锁 vs 非公平锁 核心要点:
├── 公平锁:按请求顺序(FIFO)排队获取锁,避免饥饿
├── 非公平锁:允许新线程插队,提高整体吞吐量
├── 默认选择:synchronized 和 ReentrantLock 都默认非公平
├── 实现原理:AQS 的 hasQueuedPredecessors() 是区分关键
│ ├── 公平锁:获取前检查队列是否有前驱节点
│ └── 非公平锁:直接 CAS 尝试,失败才排队
├── 性能差异:非公平锁通过减少线程唤醒开销,吞吐量显著优于公平锁
└── 公平锁选型:绝大多数场景用非公平锁,极少情况需要公平锁
参考链接
- synchronized机制详解 - synchronized 中的公平性说明
- Java读写锁-ReadWriteLock - ReentrantReadWriteLock 的公平性配置
- CAS-Compare-And-Swap - 非公平锁依赖 CAS 原子操作
- 快手电商-一面-19题总结 — Q4 中的公平锁/非公平锁考点
- 锁机制实现详解 - 锁分类体系总览