在 Java 多线程中,公平锁 和非公平锁 是 ReentrantLock 提供的两种锁获取策略,核心区别在于:多个线程竞争锁时,是否严格按照"先来后到"的顺序获取锁。
下面从原理、实现、优缺点和代码示例几个方面来讲。
一、基本概念
1. 公平锁(Fair Lock)
- 多个线程按照申请锁的顺序来获取锁,先到先得。
- 线程会进入一个 FIFO 队列排队,只有队首的线程才能获取锁。
- 新来的线程不能插队,即使锁刚好空闲,也要先看队列里有没有等待的线程。
2. 非公平锁(Nonfair Lock)
- 不保证顺序,允许插队。
- 新来的线程可以直接尝试 CAS 抢锁,如果抢到了就直接执行,不管队列里有没有等待的线程。
- 抢不到再进入队列等待。
二、源码层面的区别
ReentrantLock 内部通过 Sync 抽象类实现,有两个子类:
java
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}
// 非公平锁
static final class NonfairSync extends Sync { ... }
// 公平锁
static final class FairSync extends Sync { ... }
}
非公平锁的加锁逻辑(简化)
java
final void lock() {
// 直接 CAS 抢锁,不管有没有队列
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢不到再走正常流程
}
tryAcquire 中也不会检查队列:
java
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
公平锁的加锁逻辑
java
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;
}
}
// ... 可重入逻辑
}
hasQueuedPredecessors() 是公平锁的核心:如果队列里已经有线程在等,就不抢,乖乖排队。
三、公平锁 vs 非公平锁对比
| 维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格 FIFO | 允许插队 |
| 吞吐量 | 较低 | 较高 |
| 线程饥饿 | 不会 | 可能(理论上) |
| 上下文切换 | 较多 | 较少 |
| 实现复杂度 | 略高 | 略低 |
| 默认策略 | 否 | 是(ReentrantLock 默认非公平) |
为什么非公平锁吞吐量更高?
因为公平锁每次都要唤醒队首线程,涉及线程切换(用户态→内核态),开销大。而非公平锁允许刚释放锁的线程或新来的线程立刻抢到锁,减少了线程唤醒和切换的次数。
注意:非公平锁虽然可能"插队",但一旦进入队列,仍然是 FIFO 的,只是新来的线程有机会在入队前抢到锁。
四、代码示例
java
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
// true 表示公平锁,false 表示非公平锁
private static final ReentrantLock fairLock = new ReentrantLock(true);
private static final ReentrantLock unfairLock = new ReentrantLock(false);
public static void main(String[] args) {
testLock(fairLock, "公平锁");
System.out.println("----------");
testLock(unfairLock, "非公平锁");
}
private static void testLock(ReentrantLock lock, String name) {
Runnable task = () -> {
for (int i = 0; i < 2; i++) {
lock.lock();
try {
System.out.println(name + " -> " + Thread.currentThread().getName() + " 获取锁");
} finally {
lock.unlock();
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(task, "线程-" + i).start();
}
try { Thread.sleep(1000); } catch (InterruptedException e) { }
}
}
公平锁下,输出顺序基本是 线程-0、线程-1、线程-2... 轮转;非公平锁下顺序可能混乱,某些线程会连续获取多次。
五、其他相关点
1. synchronized 是公平锁吗?
不是 。synchronized 是非公平的,且不支持设置为公平锁。它的锁获取策略由 JVM 实现,偏向锁、轻量级锁、重量级锁都偏向于性能,不保证公平。
2. 读写锁的公平性
ReentrantReadWriteLock 也支持公平/非公平:
java
new ReentrantReadWriteLock(true); // 公平
3. 什么时候用公平锁?
- 业务要求严格按顺序执行,比如某些资源调度。
- 需要避免线程饥饿的场景。
- 大多数情况下,优先用非公平锁(默认),性能更好。
六、一句话总结
公平锁按排队顺序获取锁,避免饥饿但性能较低;非公平锁允许插队,吞吐量高但可能造成线程饥饿。
ReentrantLock默认是非公平锁,通过构造函数new ReentrantLock(true)可开启公平模式。