公平锁与非公平锁的区别与不同的使用场景

为什么非公平锁在真是场景中比公平锁要多

直接给出核心答案:为了性能(更高的吞吐量)。

在绝大多数真实场景中,非公平锁的性能要比公平锁高出 5-10 倍 甚至更多。

为了让你彻底理解,我们从现实生活的例子底层技术原理副作用三个角度来拆解。

  1. 现实生活中的例子:食堂打饭

想象一下大家在食堂排队打饭:

  • 公平锁(Fair): 窗口里的人刚打完饭离开。此时窗口空了。 就算你刚才正巧 走到窗口旁边,你也绝对不能直接插进去打饭。你必须先回头看看后面队伍里有没有人排队。如果有,你必须乖乖走到队尾去排队,让队头那个还在低头玩手机的人(被挂起的线程)走上来打饭。

  • 非公平锁(Non-Fair): 窗口里的人刚离开。 你正巧 走到窗口旁,看窗口没人,直接一步跨上去就打饭。 后面排队的人虽然心里不爽(不公平),但因为你动作很快,打个饭只要 2 秒钟,等你打完了,队头那个人才刚刚把手机收起来走到窗口前。

关键点来了: 在"非公平"的场景下,窗口(CPU)几乎没有空闲时间 ,一直在给人打饭。 而在"公平"的场景下,队头那个人从"收到信号"到"走到窗口"的这段时间,窗口是闲置的,这就浪费了资源。

  1. 技术层面的核心原因:上下文切换的开销

在计算机底层,线程的挂起(Suspend)和唤醒(Resume)是非常昂贵的操作,需要操作系统从用户态切换到内核态,这就是上下文切换(Context Switch)

A. 只有公平锁时(巨大的时间空隙)

  1. 线程 A 释放锁。

  2. AQS 队列发现队头是线程 B,于是去唤醒线程 B。

  3. 【关键耗时】 :线程 B 从"睡眠"到"真正被 CPU 调度运行"需要时间(比如 10 微秒)。在这 10 微秒里,CPU 是空闲的,锁也是空闲的

  4. 线程 B 拿到锁,开始执行。

B. 非公平锁时(填补空隙)

  1. 线程 A 释放锁。

  2. AQS 队列去唤醒线程 B。

  3. 【关键时刻】 :在线程 B 还没彻底醒过来的这 10 微秒里,线程 C 刚好请求锁

  4. 因为是非公平,线程 C 发现锁没人用,直接占有锁,开始执行。

  5. 线程 C 执行速度很快(比如只用了 5 微秒),执行完释放锁。

  6. 此时,线程 B 刚好彻底醒过来,顺利拿到锁。

结果对比:

  • 在同样的时间段内,非公平模式下,CPU 额外处理掉了线程 C 的任务。

  • 这就是为什么非公平锁能极大提升系统吞吐量(Throughput)

  1. 还有一个原因:CPU 缓存(Cache Locality)
  • 刚来的线程 C:它大概率正在 CPU 上运行,或者它的数据还在 CPU 的高速缓存(L1/L2 Cache)里,是"热"的。让它直接拿锁执行,效率极高。

  • 排队的线程 B:因为它被挂起了,它的数据可能已经从 CPU 缓存里被挤出去了,是"冷"的。唤醒它不仅慢,执行起来还需要重新加载缓存。

所以,把锁给"热"的线程 C,从系统整体利用率来看是最优解。

  1. 所谓的"非公平"究竟是怎么运作的?

很多人误以为非公平锁就是"随机乱抢",其实不是。

Java 的 ReentrantLock 实现非公平锁的逻辑是: 当一个线程请求锁时:

  1. 先尝试插队(直接 CAS 修改状态尝试拿锁)。

  2. 如果插队成功,直接拿走锁(这就是非公平的体现)。

  3. 如果锁已经被占用了,或者插队失败,它还是得乖乖去队尾排队

所以,非公平锁只是给了新来的线程一次"由于线程唤醒延迟而带来的捡漏机会"。如果没捡到漏,它依然遵循 FIFO(先进先出)的规则。

  1. 非公平锁的代价:线程饥饿(Starvation)

非公平锁这么好,为什么还需要公平锁?

因为非公平锁可能导致线程饥饿 。 如果一直有源源不断的新线程(线程 C, D, E...)过来插队,并且它们执行时间都很短,那么排在队头的线程 B 可能永远抢不到锁,一直被按在地上摩擦。

但在真实场景中:

  • 大部分业务逻辑(Web 请求、数据库操作)持有锁的时间都非常短。

  • 新线程到达的频率通常也不会极端到"无缝衔接"。

  • 所以线程 B 最终还是能拿到锁的,只是偶尔多等一会儿。

公平锁和非公平锁的使用场景

一、 非公平锁 (Non-Fair Lock) 的使用场景

口诀: 只要业务没有强制的"先来后到"要求,默认全部使用非公平锁

  1. 绝大多数的高并发业务 (Web服务器)
  • 场景描述: 像 Tomcat 处理 HTTP 请求、Spring Boot 的 Controller 逻辑。

  • 为什么:

    • 这些请求通常执行速度非常快(毫秒级)。

    • 如果在这么短的任务间强行排队,线程唤醒和切换的耗时(Context Switch)可能比执行任务本身还长

    • 使用非公平锁可以让刚释放锁的线程再次拿锁(或者新来的线程插队),利用 CPU 缓存热度,极大提高每秒处理请求数(QPS)。

  • 例子: synchronized 关键字就是强制非公平的;ReentrantLock 默认也是非公平的。

  1. 短时间的临界区 (Short Critical Sections)
  • 场景描述: 只需要修改一个状态值、从 HashMap 中取个值、或者做一个简单的计算。

  • 为什么: 拿锁 -> 改值 -> 释放锁,整个过程可能只需要几纳秒。如果为了这几纳秒去维护一个庞大的等待队列并严格按顺序唤醒,属于"杀鸡用牛刀",浪费资源。

  1. 允许少量"插队"或"饥饿"的场景
  • 场景描述: 抢红包、秒杀(在某些层面)、非严格顺序的日志记录。

  • 为什么: 用户并不在乎我是第 1000 个请求还是第 1001 个,只要系统不崩、能响应即可。个别线程多等了几毫秒,用户是无感知的。

公平锁 (Fair Lock) 的使用场景

口诀: 只有当"插队"会导致严重后果,或者持有锁的时间特别长时,才考虑公平锁。

  1. 持有锁时间很长 (Long Hold Times)
  • 场景描述: 线程拿到锁后,要执行因为 I/O 密集型操作(如读写大文件、慢速网络请求、复杂的图像处理)。

  • 为什么:

    • 如果是由于任务本身耗时长(比如 1秒),那么线程切换的开销(10微秒)就显得微不足道了。

    • 在这种情况下,非公平锁的"插队"优势消失。反而是如果允许插队,可能导致排在队尾的线程长时间拿不到锁(饥饿),最终导致请求超时报错。公平锁能确保每个线程在有限时间内都能执行。

  1. 严格要求"先来后到"的业务 (First-Come-First-Served)
  • 场景描述: * 打印机队列: 先点打印任务的必须先打印。

    • 公平交易撮合: 股票交易系统中,同价格下,先提交的订单必须先成交。

    • 排队系统: 比如银行叫号系统的内部逻辑模拟。

  • 为什么: 这是业务逻辑的硬性要求,性能必须让位于顺序。

  1. 避免"线程饥饿" (Starvation Avoidance)
  • 场景描述: 或者是系统负载极高,且每个请求都至关重要,不能接受任何一个请求因为一直被插队而超时失败。

  • 例子: 发送心跳包的线程。如果因为非公平锁导致心跳线程一直抢不到锁,可能会被误判为服务宕机,导致不必要的重启。


三、 决策指南:怎么选?

为了方便记忆,我为你总结了一个简单的决策流程图:

核心对比总结

维度 非公平锁 (Non-Fair) 公平锁 (Fair)
Java默认 synchronized, ReentrantLock() new ReentrantLock(true)
吞吐量 极高 (减少唤醒开销) 一般 (严格排队)
上下文切换 多 (每次都要唤醒队头)
饥饿现象 可能发生 (某线程一直抢不到) 几乎不会 (保证执行)
适用场景 高并发、任务短、追求性能 任务长、强顺序、防饥饿
相关推荐
heartbeat..4 小时前
Redis常见问题及对应解决方案(基础+性能+持久化+高可用全场景)
java·数据库·redis·缓存
瑞雪兆丰年兮4 小时前
[从0开始学Java|第五天]Java数组
java·开发语言
Howrun7774 小时前
C++_bind_可调用对象转化器
开发语言·c++·算法
froginwe114 小时前
PHP E-mail 发送与接收详解
开发语言
张人玉4 小时前
C#WinFrom中show和ShowDialog的区别
开发语言·microsoft·c#
m0_748233174 小时前
C#:微软的现代编程利器
开发语言·microsoft·c#
曾经的三心草4 小时前
redis-6-java客户端
java·数据库·redis
源代码•宸4 小时前
Golang面试题库(Interface、GMP)
开发语言·经验分享·后端·面试·golang·gmp·调度过程
西京刀客4 小时前
Go 语言中的 toolchain 指令-toolchain go1.23.6的作用和目的
开发语言·后端·golang·toolchain