公平锁和非公平锁的区别?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
概念理解:面试官不仅仅是想知道 "公平锁排队、非公平锁插队" 这个表面区别,更是想知道你是否理解 "公平" 的代价是什么——线程上下文切换的开销。
-
AQS 源码级理解:考察你是否看过
ReentrantLock的源码,能否说清楚FairSync和NonfairSync在.tryAcquire()方法中的具体差异。 -
工程选型意识:考察你是否知道 Java 默认用非公平锁,以及为什么绝大多数场景下非公平锁才是更好的选择。
核心答案
先甩结论:公平锁按线程等待顺序获取锁(先到先得),非公平锁允许新来的线程直接尝试 "插队" 获取锁,失败了再排队。Java 中 ReentrantLock 默认使用非公平锁,synchronized 也是非公平的。
| 对比维度 | 公平锁(FairSync) |
非公平锁(NonfairSync) |
|---|---|---|
| 获取顺序 | 严格按照 CLH 队列的等待顺序 | 新线程可直接 CAS 抢锁 |
| 吞吐量 | 较低(频繁上下文切换) | 较高(减少线程挂起/唤醒) |
| 饥饿问题 | 不会饥饿 | 可能饥饿(等待线程长期拿不到锁) |
| 实现复杂度 | 需检查队列中是否有等待线程 | 直接 CAS,失败再入队 |
| 默认使用 | 否 | 是(ReentrantLock 默认非公平) |
深度解析
一、用排队买票来类比
上面这个类比很直观:
- 公平锁就是老老实实排队,先来后到,A 办完了一定是 B 上,没人能插队
- 非公平锁允许刚到的线程直接去窗口试一把,万一窗口刚好空了,直接就办了,不用傻乎乎地先去队尾排队。如果没抢到,再去队尾老老实实排着
二、源码层面的差异
ReentrantLock 内部有两个内部类:FairSync 和 NonfairSync,它们都继承自 Sync(AQS 的子类)。关键差异在 .lock() 和 .tryAcquire() 方法上。
非公平锁的 .lock():
// NonfairSync.lock()
final void lock() {
// 上来就 CAS 抢一把,不管队列里有没有人在等
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
} else {
acquire(1); // 抢不到再走正常流程
}
}
公平锁的 .lock():
// FairSync.lock()
final void lock() {
acquire(1); // 直接走 acquire,没有 "先抢一把" 的机会
}
看到没?非公平锁一上来就 CAS 试着抢,抢到就赚了。公平锁不给你这个机会,直接进入 acquire() 流程。
再看 .tryAcquire() 的差异——这才是核心:
// NonfairSync 的 tryAcquire(调用 Sync.nonfairTryAcquire)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// state == 0 说明锁空闲,直接 CAS 抢!不管队列
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑...
return false;
}
// FairSync 的 tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// ⬇️ 关键区别!多了 hasQueuedPredecessors() 检查
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑...
return false;
}
差异就一行代码:hasQueuedPredecessors()。这个方法检查 CLH 等待队列中是否有线程排在你前面。如果有,即使锁空闲你也不能拿,得老老实实排队。这就是 "公平" 的代价。
三、为什么默认非公平?
这个设计决策值得细说。很多人直觉上觉得 "公平肯定更好啊",但其实不然:
非公平锁吞吐量更高。 原因在于线程上下文切换的成本。假设线程 A 刚释放锁,此时队列中 B 正在等待。如果是公平锁,流程是:
A 释放锁 → 唤醒 B(内核态切换)→ B 从等待态恢复(内核态切换)→ B 获取锁
而非公平锁,如果恰好有个线程 C 正在 CPU 上运行:
A 释放锁 → C 直接 CAS 抢到(用户态,几乎无开销)
你看,非公平锁省掉了 "唤醒 B" 和 "B 恢复执行" 两次昂贵的上下文切换。JDK 的设计者经过测试,发现非公平锁的吞吐量比公平锁高出 一个数量级(参考 Doug Lea 的论文)。
当然,代价就是公平性——B 可能被饿死。但在绝大多数业务场景中,这点不公平完全可以接受。
四、什么时候用公平锁?
虽然非公平锁是默认选择,但以下场景确实需要公平锁:
- 定时任务调度:任务执行顺序敏感,先提交的任务应该先执行
- 资源分配:多个服务竞争共享资源,需要保证公平性
- 限流/配额控制:不能让某些请求永远 "插队" 成功
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true); // true = 公平
// 创建非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock(); // 默认 false
ReentrantLock unfairLock2 = new ReentrantLock(false); // 显式指定
面试高频追问
-
synchronized是公平锁还是非公平锁?非公平锁。
synchronized底层基于Monitor实现,线程唤醒后谁先抢到Monitor全凭运气,不保证等待顺序。 -
ReentrantReadWriteLock的读锁和写锁是公平的吗?同样支持公平/非公平两种模式,通过构造函数参数控制。默认也是非公平的。但在公平模式下,读锁也不能插队——必须等队列中前面的写锁释放后才能获取。
-
AQS 的 CLH 队列是什么?
CLH(Craig, Landin, and Hagersten)队列是 AQS 内部维护的一个 双向链表,用来管理等待获取锁的线程。每个节点(
Node)包含线程引用、等待状态等信息。公平锁就是严格按这个链表的 FIFO 顺序来调度线程。
常见面试变体
- "为什么
ReentrantLock默认用非公平锁?" - "公平锁有什么缺点?"
- "
synchronized是公平锁吗?" - "AQS 是怎么实现公平锁的?"
记忆口诀
区别:公平排队不插队,非公平先抢再排队
选型:默认非公平求吞吐,业务需要公平再开启
源码差异:就差一个 hasQueuedPredecessors()(公平锁多了 "队里有没有人" 的检查)
总结
公平锁和非公平锁的核心区别在于获取锁时是否检查等待队列。公平锁通过 hasQueuedPredecessors() 保证先到先得,但代价是更多的上下文切换和更低的吞吐量;非公平锁允许新线程直接 CAS "插队",性能更好但可能导致线程饥饿。Java 默认非公平,这是 Doug Lea 经过充分测试后的工程决策。面试时把源码差异、为什么默认非公平、什么时候该用公平锁这三点讲清楚,基本满分。
