问题
公平锁和非公平锁的区别?
答案
1. 核心概念
公平锁(Fair Lock):
- 多个线程按照申请锁的顺序获取锁,先到先得(FIFO)
- 等待时间最长的线程优先获得锁
非公平锁(Unfair Lock):
- 线程获取锁时不考虑等待队列,直接尝试获取锁
- 可能出现”插队”现象,后来的线程可能先获得锁
通俗理解:
- 公平锁:像排队买票,先来的先买
- 非公平锁:不排队,谁抢到谁买(新来的人可能先买到)
2. 实现对比
2.1 ReentrantLock 的两种模式
ReentrantLock 支持公平和非公平两种模式:
// 非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
// 或
ReentrantLock unfairLock = new ReentrantLock(false);
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
2.2 非公平锁的实现
// ReentrantLock 的非公平锁实现
static final class NonfairSync extends Sync {
final void lock() {
// 1. 直接尝试 CAS 获取锁(插队)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // CAS 失败,进入正常流程
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 锁未被占用
// 2. 不检查队列,直接 CAS 获取锁(插队)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入逻辑
int nextc = c + acquires;
setState(nextc);
return true;
}
return false;
}
关键点:
- 不检查等待队列,直接尝试 CAS 获取锁
- 新来的线程可能”插队”成功,跳过等待队列中的线程
2.3 公平锁的实现
// ReentrantLock 的公平锁实现
static final class FairSync extends Sync {
final void lock() {
acquire(1); // 直接进入正常流程,不尝试插队
}
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;
setState(nextc);
return true;
}
return false;
}
}
// 检查队列中是否有前驱节点
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 队列不为空 && (头节点的下一个节点为空 || 下一个节点的线程不是当前线程)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
关键点:
- 检查等待队列:通过
hasQueuedPredecessors()判断是否有线程在排队 - 只有队列为空或当前线程是队首,才能获取锁
- 严格按照 FIFO 顺序分配锁
3. 执行流程对比
3.1 非公平锁流程
线程 A 持有锁
|
线程 B、C、D 在队列中等待
|
线程 A 释放锁
|
线程 E 新到达 ──┐
├──> 线程 E 直接尝试 CAS 获取锁(插队)
│
├──> 成功:线程 E 获得锁(跳过 B、C、D)
│
└──> 失败:线程 E 加入队列尾部,线程 B 获得锁
示例代码:
public class UnfairLockExample {
private static ReentrantLock lock = new ReentrantLock(false); // 非公平锁
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
lock.unlock();
}
}
};
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
Thread t3 = new Thread(task, "线程3");
t1.start();
t2.start();
t3.start();
}
}
// 可能的输出(非公平,顺序不确定):
// 线程1 获得锁
// 线程1 获得锁 ← 线程1 释放后立即重新获得(插队)
// 线程2 获得锁
// 线程3 获得锁
// 线程2 获得锁
// 线程3 获得锁
3.2 公平锁流程
线程 A 持有锁
|
线程 B、C、D 在队列中等待(按顺序)
|
线程 A 释放锁
|
线程 E 新到达 ──┐
├──> 线程 E 检查队列
│
├──> 发现有 B、C、D 在等待
│
└──> 线程 E 加入队列尾部,线程 B 获得锁(FIFO)
示例代码:
public class FairLockExample {
private static ReentrantLock lock = new ReentrantLock(true); // 公平锁
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得锁");
} finally {
lock.unlock();
}
}
};
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
Thread t3 = new Thread(task, "线程3");
t1.start();
t2.start();
t3.start();
}
}
// 可能的输出(公平,基本按顺序):
// 线程1 获得锁
// 线程2 获得锁 ← 公平分配
// 线程3 获得锁
// 线程1 获得锁
// 线程2 获得锁
// 线程3 获得锁
4. 详细对比
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取锁顺序 | 严格按 FIFO 顺序 | 不保证顺序,可能插队 |
| 是否检查队列 | ✅ 检查队列是否有前驱节点 | ❌ 不检查队列,直接 CAS |
| 吞吐量 | 低(频繁的上下文切换) | 高(减少上下文切换) |
| 性能 | 较低 | 较高 |
| 线程饥饿 | ✅ 不会饥饿(保证公平) | ❌ 可能饥饿(长时间得不到锁) |
| 实现复杂度 | 较高(需要维护队列顺序) | 较低 |
| 默认模式 | ReentrantLock 非默认 | ReentrantLock 默认 |
5. 性能差异
5.1 非公平锁性能更好的原因
原因 1:减少线程切换
非公平锁:
线程 A 释放锁 → 线程 A 立即重新获得锁(不切换)
公平锁:
线程 A 释放锁 → 唤醒线程 B → 线程 B 获得锁(上下文切换)
原因 2:减少唤醒开销
- 非公平锁:如果新线程能立即获得锁,无需唤醒队列中的线程
- 公平锁:每次都需要唤醒队列中的下一个线程
性能测试结果:
- 非公平锁的吞吐量通常是公平锁的 10-100 倍(高竞争场景)
- 但非公平锁可能导致某些线程长时间无法获得锁(饥饿)
6. synchronized 是公平还是非公平
synchronized 是非公平锁:
- 不保证等待线程的获取顺序
- JVM 的实现不遵循 FIFO 原则
- 优先考虑性能而非公平性
public synchronized void method() {
// 非公平锁,不保证顺序
}
7. 应用场景
7.1 使用非公平锁
场景:
- 对吞吐量要求高
- 不关心线程获取锁的顺序
- 任务执行时间较短
示例:
- 缓存操作
- 统计计数器
- 高频率的读写操作
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
7.2 使用公平锁
场景:
- 需要严格按顺序处理请求
- 防止线程饥饿
- 任务执行时间较长
示例:
- 订单处理系统(先来先服务)
- 任务调度系统
- 需要保证顺序的业务逻辑
ReentrantLock lock = new ReentrantLock(true); // 公平锁
8. 线程饥饿问题
8.1 非公平锁的饥饿风险
public class StarvationExample {
private static ReentrantLock lock = new ReentrantLock(false); // 非公平锁
public static void main(String[] args) {
// 10 个高频线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
while (true) {
lock.lock();
try {
// 快速执行
} finally {
lock.unlock();
}
}
}, "高频线程" + i).start();
}
// 1 个低频线程
new Thread(() -> {
lock.lock();
try {
System.out.println("低频线程终于获得锁"); // 可能很久才打印
} finally {
lock.unlock();
}
}, "低频线程").start();
}
}
问题:高频线程不断”插队”,低频线程可能长时间无法获得锁。
8.2 公平锁避免饥饿
ReentrantLock lock = new ReentrantLock(true); // 公平锁
// 低频线程按顺序排队,最终一定能获得锁
9. 性能优化建议
- 默认使用非公平锁:在大多数情况下性能更好
- 特殊场景用公平锁:需要严格顺序或防止饥饿时
- 减少锁持有时间:无论公平与否,快速释放锁都能提升性能
- 考虑读写锁:如果读多写少,使用
ReentrantReadWriteLock
10. 总结
公平锁:
- 严格按 FIFO 顺序分配锁
- 保证公平性,避免饥饿
- 性能较低(频繁的上下文切换)
- 适用于对顺序敏感的场景
非公平锁:
- 不保证顺序,允许”插队”
- 吞吐量高,性能好
- 可能导致线程饥饿
- Java 默认选择(synchronized、ReentrantLock 默认)
面试要点:
- 定义区别:公平锁按顺序,非公平锁可插队
- 实现差异:公平锁检查队列(
hasQueuedPredecessors()),非公平锁直接 CAS - 性能对比:非公平锁性能显著优于公平锁(10-100 倍)
- 应用场景:默认用非公平锁,特殊场景用公平锁
- synchronized:属于非公平锁
记忆口诀:
- 公平排队讲顺序,非公平插队效率高
- 默认非公平性能好,特殊场景公平保