公平锁和非公平锁的区别?
2026年01月14日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常意在考察以下几个核心维度:
- 对锁 “公平性” 概念的理解:候选人能否准确阐述公平锁和非公平锁在 “线程获取锁的顺序” 上的本质区别。
- 对性能与开销的权衡意识:面试官不仅仅是想知道定义,更是想知道候选人是否理解这两种策略在性能(吞吐量、延迟)和系统开销(上下文切换)上的差异及其原因。
- 底层实现机制的了解:候选人是否了解
ReentrantLock等锁是如何利用 AQS(AbstractQueuedSynchronizer)的同步队列来实现这两种策略的。 - 实际场景的应用能力:考察候选人能否根据具体业务场景(如对响应时间要求极高、或需要严格按序执行)做出合理的选择,这反映了其工程实践经验。
核心答案
公平锁与非公平锁的核心区别在于 线程获取锁的调度策略:
- 公平锁:多个线程按照 申请锁的绝对时间顺序 来排队获取锁。锁释放后,会优先唤醒在同步队列中等待时间最长的线程(队头线程)。这保证了获取锁的 “先来后到”,是公平的。
- 非公平锁:线程在尝试获取锁时,可以 “插队”。无论同步队列中是否有其他线程在等待,新来的线程都有机会直接尝试竞争锁。如果竞争失败,才会加入队列尾部排队。这种策略不保证申请锁的时间顺序。
在 Java 中,ReentrantLock 可以通过构造函数 new ReentrantLock(true) 创建公平锁,默认(或 new ReentrantLock(false))创建的是非公平锁。synchronized 关键字实现的锁是非公平锁。
深度解析
原理/机制
其底层实现依赖于 AQS 内部的 CLH 变体同步队列。
- 公平锁的
tryAcquire逻辑:在尝试获取锁时,会先检查 AQS 同步队列中是否有前驱节点在等待(hasQueuedPredecessors())。如果有,则当前线程自觉加入队列尾部排队,不会尝试竞争。这确保了 “先到先得”。 - 非公平锁的
tryAcquire逻辑:在尝试获取锁时,不会检查队列。无论队列是否为空,都会直接通过 CAS 操作去抢锁。这给了新线程“插队”的机会。
代码示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairAndUnfairLockDemo {
public static void main(String[] args) {
// 分别测试公平锁和非公平锁
testLock(new ReentrantLock(true), “公平锁”);
testLock(new ReentrantLock(false), “非公平锁”); // 默认
}
private static void testLock(Lock lock, String lockType) {
System.out.println("\n=== 测试: " + lockType + " ===");
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) { // 每个线程获取两次锁
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
Thread.sleep(100); // 模拟持有锁一段时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "Thread-" + i).start();
}
// 等待所有线程执行完毕
try { Thread.sleep(3000); } catch (InterruptedException e) {}
}
}
运行上述代码,你可以观察到:在公平锁模式下,输出中线程获取锁的顺序更接近其启动顺序,规律性强;而在非公平锁模式下,输出顺序可能混乱,经常出现一个线程连续获取锁的情况(“线程饥饿” 现象),这直观展示了 “插队” 行为。
对比分析与最佳实践
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格按照 FIFO(先入先出) | 允许 “插队”,不保证顺序 |
| 吞吐量 | 相对较低 | 相对更高(通常是非公平锁的几倍) |
| 线程饥饿 | 不会发生 | 可能发生 |
| 上下文切换 | 较多,线程频繁排队/唤醒 | 相对较少,新线程可能直接获取锁成功 |
- 为何非公平锁性能通常更高? 核心原因在于 减少了线程的挂起和唤醒次数。当持有锁的线程释放锁时,如果恰好有一个新线程来请求,这个新线程可以立即获取锁并执行,避免了将自身挂起、再被唤醒的开销(这是一个耗时的内核态操作)。此外,这种“插队”机制也更好地利用了 CPU 的时间片,提升了整体吞吐量。
- 最佳实践:默认情况下,优先使用非公平锁。在
ReentrantLock的文档中明确建议,除非对公平性有特殊要求,否则使用非公平锁能获得更高的吞吐量。只有在必须保证线程执行的严格顺序(例如,防止低优先级线程长期无法执行),且能接受一定性能损耗的场景下,才考虑使用公平锁。
常见误区
- 误区一:非公平锁不公平,所以不好。 这是一种教条的理解。在绝大多数并发场景下,“不公平” 带来的性能收益远大于其理论上的缺点。系统设计是在做权衡(Trade-off)。
- 误区二:
synchronized是公平锁。 不对,synchronized的实现是典型的非公平锁,它不提供任何公平性保证。 - 误区三:公平锁能完全解决线程饥饿。 虽然公平锁在锁的获取层面解决了饥饿,但如果线程在临界区内执行时间过长,依然会影响整个系统的响应性。
总结
简单来说,公平锁讲究秩序,性能有代价;非公平锁鼓励竞争,吞吐量更高。在实际开发中,非公平锁是更普适和高效的选择,而公平锁仅适用于对线程执行顺序有严格要求的特定场景。