在 Java 显式锁体系中,ReentrantLock 相比内置 synchronized 最核心的差异化能力,就是支持公平锁与非公平锁双模式切换。两者都是独占可重入锁,本质差异只有一个:锁被释放时,新来的线程是直接插队抢锁,还是必须排到等待队列末尾等。
很多开发者对两者的认知停留在「公平 = 排队、非公平 = 插队」的表层,对底层实现、性能差异、选型逻辑、默认设计初衷理解模糊,甚至滥用公平锁导致性能瓶颈,或者误用非公平锁导致业务饥饿。本文从定义差异、底层实现、优缺点权衡、默认设计逻辑、生产选型指南五个维度,讲透两者的本质区别与落地最佳实践。
一、核心定义与本质差异
两种模式都基于 AQS 独占模式实现,都支持可重入特性,释放锁,唤醒流程都一致,核心分歧在于锁竞争的调度策略:
公平锁:严格遵循先来先到,所有线程按请求顺序排成 FIFO 队列,先到先得,绝对不允许插队
非公平锁:锁释放时,新来的线程可以直接尝试抢锁,不用先排队,抢不到再进入队列尾部,允许 “插队”
两者没有绝对的优劣之分,只是公平性与吞吐量的不同取舍:公平锁保证顺序牺牲性能,非公平锁牺牲绝对公平换取更高的执行效率。
二、公平锁(Fair Lock):严格排队的先来先得锁
2.1 大白话功能与生活化例子
公平锁是严格按申请顺序执行的锁。所有线程按照发起加锁请求的先后,排成 FIFO 等待队列;锁释放时,只有队列最前面的线程能被唤醒并拿到锁,新来的线程必须排到队列末尾,绝对不允许插队抢占。
生活化例子:银行普通窗口叫号办理业务,所有人先取号排队,叫到号才能办理;哪怕窗口刚好空出来,后来的人也必须取号排到最后,不能直接上前插队,绝对保证先来后到。
2.2 底层实现原理
公平锁基于 AQS 独占模式实现,核心通过队列前置校验保证公平性,完整执行逻辑:
线程调用
lock()尝试加锁时,首先判断 AQS 的state变量是否为 0(锁是否空闲)。如果锁空闲,先执行
hasQueuedPredecessors()校验:队列中已有等待线程:当前线程不能抢锁,必须封装为等待节点排到队列尾部,调用
LockSupport.park()阻塞等待队列中无等待线程:才通过 CAS 原子操作尝试加锁,成功则标记当前线程为锁的独占持有者
如果锁已被占用,当前线程直接封装节点加入队列尾部,进入阻塞等待状态。
锁释放时,仅唤醒队列头部的下一个等待线程,按顺序依次拿锁,不会批量唤醒、也不会给新来的线程插队机会。
核心细节:
hasQueuedPredecessors()是公平性的核心保障方法,只要同步队列里有线程在等待,新来的线程就不能抢占锁,必须排队,从机制上彻底保证先来先得。
2.3 核心优缺点
优点 | 缺点 |
绝对先来先到,无线程饥饿风险,所有线程最终都能拿到锁 | 吞吐量低,大量线程上下文切换,整体性能比非公平锁低 20%~50% |
执行顺序可预测,调度行为固定,问题排查简单 | 锁空闲时新来线程也不能用,存在资源空转浪费,锁利用率低 |
逻辑确定性强,适合顺序强一致的业务场景 | 高并发场景下队列积压严重,整体处理效率下降明显 |
2.4 选型思考
适合对执行顺序有严格要求、低并发、公平性优先级高于性能的场景,比如按请求顺序处理的任务队列、公平资源分配、有严格时序要求的状态流转。
选型原则:只有业务明确要求先来先到、必须保证执行顺序时,才选公平锁;没有明确顺序要求的场景,一律不选公平锁,避免不必要的性能损耗。
三、非公平锁(Nonfair Lock):吞吐量优先的插队锁
3.1 大白话功能与生活化例子
非公平锁是允许插队、效率优先的锁。锁被释放时,不管队列里有没有等待的线程,新来的线程都可以直接尝试 CAS 抢锁,抢到就直接执行业务,抢不到再排到队列末尾等待。核心目标是让锁尽量不空闲,最大化锁的利用率。
生活化例子:便利店自助结账机,没人结账的时候你过来直接就能用,不用看有没有人在后面排队;刚好有人结完账,你刚好走到就可以直接使用,不用严格按先来后到等待。核心是让结账台(锁)尽量不闲着,提升整体效率。
3.2 底层实现原理
非公平锁是 ReentrantLock 的默认实现,同样基于 AQS 独占模式,核心逻辑是锁空闲时优先抢锁,抢不到再排队:
线程调用
lock()尝试加锁时,不做任何队列校验,直接用 CAS 尝试把state从 0 修改为 1,尝试直接抢占锁。如果 CAS 成功(抢到锁),直接标记当前线程为锁持有者,立刻执行业务逻辑,完全不用排队。
如果 CAS 失败(锁被占用 / 抢锁失败),才把当前线程封装成等待节点,加入同步队列尾部,阻塞等待唤醒。
锁释放时,唤醒队列头部的等待线程;但唤醒过程中如果有新线程过来抢锁,新线程有可能先抢到锁,被唤醒的线程可能再次抢锁失败,继续等待。
核心细节:非公平锁去掉了
hasQueuedPredecessors()前置校验,这是和公平锁最核心的代码差异。虽然允许插队,但 AQS 队列里的等待线程最终还是会被唤醒,不会永久饥饿,只是可能被新来的线程多次插队,等待时间变长。
3.3 核心优缺点
优点 | 缺点 |
吞吐量高,锁利用率高,减少线程上下文切换,整体性能优于公平锁 | 存在线程饥饿风险,高并发下部分线程可能长时间抢不到锁 |
锁空闲时立刻能被使用,无资源空转浪费,执行效率高 | 执行顺序不可预测,调度不确定性强,不适合顺序敏感业务 |
高并发场景下整体处理能力强,适配绝大多数通用业务 | 偶发问题排查难度高,执行顺序不固定,问题难复现 |
3.4 选型思考
适合高并发、吞吐量优先、无严格顺序要求的通用业务场景,比如普通数据更新、资源独占加锁、接口并发控制,这是绝大多数业务场景的选择。
选型原则:没有明确公平性要求的场景,默认选非公平锁,用少量的公平性不确定性换取大幅的性能提升,投入产出比最高。
四、核心差异对照表
对比维度 | 公平锁 | 非公平锁 |
调度核心原则 | 严格先来先到,FIFO 队列顺序执行 | 允许插队,锁空闲时新来线程可直接抢占 |
核心实现逻辑 |
| 无队列校验,锁空闲直接 CAS 抢锁 |
是否为默认模式 | ❌ 不是默认 | ✅ 官方默认模式 |
整体吞吐量 | 低,线程切换与队列开销大 | 高,减少空转与上下文切换损耗 |
线程饥饿风险 | 无,绝对先来先得 | 有,高并发下部分线程等待时间变长 |
执行顺序特性 | 可预测、固定顺序 | 不可预测、调度灵活 |
性能损耗 | 高,大量阻塞唤醒切换 | 低,锁利用率最大化 |
核心适用场景 | 顺序敏感、低并发、公平优先 | 高并发、吞吐量优先、无顺序要求 |
五、默认模式是什么?为什么这么设计?
5.1 默认模式结论
ReentrantLock默认是非公平锁。无参构造方法直接创建的就是非公平锁,只有传入true参数时才创建公平锁。
// 默认创建非公平锁 ReentrantLock lock = new ReentrantLock(); // 手动指定创建公平锁 ReentrantLock fairLock = new ReentrantLock(true);5.2 为什么默认选择非公平锁?(架构师设计思考)
这不是随意的设计,而是 JDK 团队基于工业界大量实践验证的工程权衡,核心有三大原因:
性能优先的通用设计绝大多数业务场景,吞吐量、执行效率的优先级远高于绝对公平。非公平锁的吞吐量比公平锁高出 30%~50%,性能优势非常明显。默认选择性能更好的方案,符合类库设计的「默认最优体验」原则,让大多数开发者不用额外配置就能拿到最好的性能。
公平锁适用场景极少真实业务中,对锁的执行顺序有严格要求的场景非常少,绝大多数场景只需要保证线程安全、不出现并发错误,不需要严格的先来先到。默认适配绝大多数通用场景,减少开发者的配置成本与学习成本。
饥饿风险整体可控非公平锁不是完全不公平,只是允许新来的线程在锁空闲的瞬间插队。如果抢锁失败,线程还是会进入队列排队,等待线程最终都会被唤醒,不会出现永久饥饿。只是极端高并发下部分线程等待时间变长,属于可接受的工程权衡。
补充佐证:内置锁 synchronized 的底层实现也是非公平调度,这也侧面说明:通用并发场景下,非公平是行业默认的最优选择,性能优先级高于绝对公平。
六、选型指南与生产避坑
6.1 选型口诀
无明确顺序要求 → 选默认非公平锁,性能优先
必须先来先到 → 选公平锁,保证执行顺序
高并发吞吐量场景 → 优先非公平锁
低并发顺序敏感场景 → 选用公平锁
6.2 生产避坑指南
禁止盲目使用公平锁:不要为了 “看起来更合理” 就用公平锁,绝大多数业务场景不需要严格顺序,平白损失 30%+ 性能。
禁止非公平锁处理顺序敏感业务:比如按顺序扣款、按请求优先级处理队列,用非公平锁会导致顺序错乱,引发业务逻辑异常。
重入不改变公平模式:同一个线程多次加锁(可重入),不会破坏公平性,也不会切换锁的调度模式,两种模式都完全支持重入。(注:重入多次意味着需要解锁多次,否则会导致锁无法释放)
避免长事务 + 公平锁组合:公平锁本身调度慢,再加长事务长时间持有锁,会导致队列严重积压,吞吐量暴跌。
七、总结
公平锁与非公平锁没有绝对的好坏,只是不同场景下的不同取舍:公平锁牺牲性能换顺序确定性,非公平锁牺牲绝对公平换高吞吐量。
ReentrantLock 默认选择非公平锁,是工业界经过大量实践验证的最优选择 —— 通用场景下,性能的价值远高于绝对公平。架构选型的核心原则永远是:业务需求驱动,优先匹配场景,而非追求绝对公平或绝对性能。没有最好的锁,只有最适配业务的锁。