在 Java 并发编程中,ReentrantLock 和 synchronized 默认采用的都是非公平锁。许多开发者因此形成了一种刻板印象:非公平锁性能更好,是“默认且最佳”的选择。
然而,非公平锁并不一定在所有场景下都更好。虽然它在吞吐量上占据优势,但其固有的“插队”机制可能导致线程饥饿。本文将深入探讨非公平锁的局限性,并分析何时必须使用公平锁。
一、 为什么非公平锁是默认选择?
非公平锁(Non-fair Lock)允许新来的线程在锁释放的瞬间,直接通过 CAS 尝试获取锁,而无需进入等待队列 。这种机制带来了显著的性能优势:
- 更高的吞吐量:减少了线程上下文切换的开销。唤醒一个阻塞线程需要昂贵的内核态与用户态切换,如果新线程能直接获取锁并快速执行,就避免了这一过程 。
- 整体效率更高:在高竞争环境下,非公平锁的吞吐率通常远高于公平锁,因此在大多数通用场景下,它是更优解 。
二、 非公平锁的致命弱点:线程饥饿
非公平锁的核心缺陷在于不公平性可能导致线程饥饿 。
- 插队现象:如果某些线程运气不好,每次准备获取锁时都有新线程“插队”,它们可能长期无法获得锁 。
- 不确定性:获取锁的时间变得不可预测,对于对响应时间有严格要求的系统,这可能引发严重的延迟问题 。
三、 什么时候必须使用公平锁?
尽管公平锁(Fair Lock)因频繁的上下文切换导致吞吐量较低,但在以下场景中,它是不可或缺的选择:
1. 任务执行顺序敏感(如定时任务调度)
如果业务逻辑严格要求任务必须按照提交的顺序执行,公平锁是唯一选择。
- 场景:定时任务调度、消息队列消费、数据库事务日志重放。
- 原因:非公平锁可能导致后提交的任务先执行,破坏业务的时间序或因果序,导致数据不一致 。
2. 防止资源竞争中的线程饥饿
当多个服务或线程竞争共享资源,且持有锁的代码执行时间较长时,非公平锁可能导致部分线程“饿死”。
- 场景:分布式锁竞争、共享连接池获取、硬件资源访问。
- 原因:公平锁通过维护 FIFO(先进先出)队列,确保每个等待的线程最终都能获得锁,避免长期垄断 。
3. 限流与配额控制(保证 QoS)
在需要保证服务质量(QoS)的场景下,不能让某些请求永远“插队”成功。
- 场景:API 网关速率限制、金融交易订单撮合。
- 原因:非公平锁的等待时间是随机的,可能出现极长的尾延迟。公平锁提供了可预测的响应时间上限,确保所有用户请求得到公平对待 。
四、 总结与建议
| 特性 | 公平锁 (Fair Lock) | 非公平锁 (Non-fair Lock) |
|---|---|---|
| 获取顺序 | 严格 FIFO | 随机,允许插队 |
| 吞吐量 | 较低 | 较高 |
| 线程饥饿 | 不会发生 | 可能发生 |
| 适用场景 | 顺序敏感、防饥饿、长任务 | 高并发、短任务、追求高性能 |
结论:
基于对性能和公平性的权衡,如果你的应用对极致吞吐量敏感且任务简短,非公平锁是首选 。但如果你的系统需要保证顺序、防止饥饿或提供可预测的响应时间,则必须使用公平锁 。
在实际架构设计中,除了切换锁策略,还可以考虑使用细粒度锁或异步队列(如 Disruptor)来进一步优化并发性能,从而在公平性与效率之间找到最佳平衡点。
评论区