公平锁和非公平锁的区别?
公平锁与非公平锁的核心区别在于 线程获取锁的调度策略:
- 公平锁:多个线程按照 申请锁的绝对时间顺序 来排队获取锁。锁释放后,会优先唤醒在同步队列中等待时间最长的线程(队头线程)。这保证了获取锁的 “先来后到”,是公平的。
- 非公平锁:线程在尝试获取锁时,可以 “插队”。无论同步队列中是否有其他线程在等待,新来的线程都有机会直接尝试竞争锁。如果竞争失败,才会加入队列尾部排队。这种策略不保证申请锁的时间顺序。
在 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的实现是典型的非公平锁,它不提供任何公平性保证。 - 误区三:公平锁能完全解决线程饥饿。 虽然公平锁在锁的获取层面解决了饥饿,但如果线程在临界区内执行时间过长,依然会影响整个系统的响应性。
浙公网安备 33010602011771号