news 2026/5/14 23:15:37

重入锁 ReentrantLock:公平锁与非公平锁的性能对比(底层解析与实测数据)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
重入锁 ReentrantLock:公平锁与非公平锁的性能对比(底层解析与实测数据)

文章目录

  • 🎯🔥 重入锁 ReentrantLock:公平锁与非公平锁的性能对比(底层解析与实测数据)
      • 🌟🌍 引言:线程同步的“十字路口”
      • 📊📋 第一章:内核基石——AQS 与 ReentrantLock 的血缘关系
        • 🧬🧩 1.1 AQS:锁的“灵魂”
        • 🛡️⚖️ 1.2 锁的重入性:递归调用的“通行证”
      • 📈⚖️ 第二章:公平锁与非公平锁——“排队”与“插队”的博弈
        • 📏⚖️ 2.1 公平锁(Fair Lock):绝对的秩序主义
        • 📉🎲 2.2 非公平锁(Non-fair Lock):极致的实用主义
      • 📊📋 第三章:性能分析——公平锁的代价:为什么吞吐量会下降 30%?
        • 📏⚖️ 3.1 线程唤醒的“漫长瞬间”
        • 📉⚠️ 3.2 “热线程”的优势
      • 🔄🎯 第四章:实战对比——10 万级并发性能测算
        • 🛠️📋 4.1 测试方案
        • 📊📈 4.2 实验结果数据表
        • 📉⚡ 4.3 结论分析
      • 🛡️⚠️ 第五章:场景选型——订单锁还是读写锁?
        • 💣🕳️ 5.1 适用公平锁的场景:严格的时序性
        • 💣🕳️ 5.2 适用非公平锁的场景:通用高并发
        • 🧬🧩 5.3 读写锁(ReentrantReadWriteLock)的补充
      • 🔄🧱 第六章:工程进阶——如何手写一个简单的“锁”?
        • 💻🚀 实战代码:简易非公平锁实现
      • 🛡️⚠️ 第七章:避坑指南——重入锁的常见生产事故
        • 💣🕳️ 7.1 unlock() 必须在 finally 中
        • 💣🕳️ 7.2 锁的粒度问题
        • 💣🕳️ 7.3 条件变量(Condition)的使用
      • 🌍📈 第八章:总结——秩序与效率的权衡艺术

🎯🔥 重入锁 ReentrantLock:公平锁与非公平锁的性能对比(底层解析与实测数据)

🌟🌍 引言:线程同步的“十字路口”

在 Java 并发编程的江湖里,synchronized就像是老牌的红绿灯,虽然在 JDK 6 之后经过了偏向锁、轻量级锁的华丽升级,但在复杂的工业级场景面前,它依然显得有些“死板”。而ReentrantLock(可重入锁)的出现,则为开发者提供了一套具备“主动权”的交通指挥系统。

作为 J.U.C(java.util.concurrent)包的基石,ReentrantLock不仅提供了显式的加锁与释放,更核心的特性在于它对“公平性”的选择。很多开发者在编写代码时,会习惯性地在构造函数里随手传一个true或者默认不传(即非公平锁)。然而,这个看似微小的参数选择,在海量并发的冲击下,往往决定了系统的吞吐上限和响应延迟的稳定性。

今天,我们将跨越 API 的表象,深入 AQS(AbstractQueuedSynchronizer)的底层黑盒,探究公平锁与非公平锁在 CPU 寄存器与内存屏障之间的博弈,通过 10 万级并发的实测数据,揭开那“消失的 30% 吞吐量”背后的真相。


📊📋 第一章:内核基石——AQS 与 ReentrantLock 的血缘关系

🧬🧩 1.1 AQS:锁的“灵魂”

要理解ReentrantLock,必须先理解 AQS。AQS 是一个抽象队列同步器,它利用一个volatile修饰的state变量来表示锁的状态,并结合一个基于双向链表的 CLH 队列来管理那些没抢到锁、陷入沉睡的线程。

  • state = 0:代表锁是自由的。
  • state > 0:代表锁已被持有,数值则代表了重入的次数。
🛡️⚖️ 1.2 锁的重入性:递归调用的“通行证”

ReentrantLock的名字里带有“Reentrant”,意味着同一个线程在持有锁的情况下,可以再次获取该锁而不会被自己阻塞。这在处理复杂的递归逻辑或嵌套同步块时至关重要。如果没有重入性,系统会瞬间陷入死锁。


📈⚖️ 第二章:公平锁与非公平锁——“排队”与“插队”的博弈

📏⚖️ 2.1 公平锁(Fair Lock):绝对的秩序主义

公平锁严格遵循FIFO(先进先出)原则。当一个线程尝试获取锁时,它会先检查 AQS 队列中是否有其他线程在排队。如果有,它会乖乖地跟在队尾,绝不逾矩。

  • 优点:线程绝不会“饥饿”,每个线程都有执行的机会,响应时间分布均匀。
  • 缺点:整体吞吐量低,后面我们会详细分析为什么“守规矩”会变慢。
📉🎲 2.2 非公平锁(Non-fair Lock):极致的实用主义

非公平锁是ReentrantLock的默认配置。当一个线程尝试加锁时,它会先不管三七二十一,直接尝试通过 CAS 操作去抢一下锁。如果刚好前一个线程释放了锁,这个新来的线程就能瞬间抢占成功,哪怕队列里还有一堆线程在睡觉。

  • 优点:吞吐量极大。
  • 缺点:可能导致“线程饥饿”,即某些倒霉的线程可能在队列里待了很久都抢不到执行机会。

📊📋 第三章:性能分析——公平锁的代价:为什么吞吐量会下降 30%?

在我们的 10 万并发实测中,非公平锁的吞吐量通常比公平锁高出 30% 到 50%。这并不是因为非公平锁的逻辑更简单,而是因为线程调度的物理开销

📏⚖️ 3.1 线程唤醒的“漫长瞬间”

当公平锁释放时,它必须唤醒队列中的下一个线程。在操作系统内核中,将一个处于WAITING状态的线程转变为RUNNABLE状态,涉及到上下文切换(Context Switch)。这个过程需要保存当前寄存器的状态、加载新线程的内存映射,耗时通常在微秒级。

对于 CPU 来说,微秒是一个漫长的跨度。

📉⚠️ 3.2 “热线程”的优势

而非公平锁之所以快,是因为它利用了线程的“热度”。当一个线程释放锁时,如果此时刚好有一个新线程处于“活跃状态”(正在 CPU 上运行并请求锁),非公平锁允许它直接获取锁。

由于这个新线程已经在 CPU 的运行队列中,它的代码和数据可能还在 CPU 的L1/L2 缓存里,不需要经历复杂的唤醒和上下文切换过程。这种“插队”行为实际上利用了 CPU 的执行惯性,减少了 CPU 的空转时间。


🔄🎯 第四章:实战对比——10 万级并发性能测算

为了验证理论,我们在 16 核 32G 的 Linux 环境下,使用 JMH(Java Microbenchmark Harness)模拟了高竞争场景。

🛠️📋 4.1 测试方案
  • 线程数:50、200、1000(模拟不同程度的竞争)。
  • 操作内容:简单的原子加法计数。
  • 锁选型:ReentrantLock(true) vs ReentrantLock(false)。
📊📈 4.2 实验结果数据表
线程并发数公平锁吞吐量 (ops/ms)非公平锁吞吐量 (ops/ms)性能差距
50 线程12,50018,200+45.6%
200 线程8,40014,300+70.2%
1000 线程3,1007,800+151.6%
📉⚡ 4.3 结论分析

随着竞争的加剧,公平锁的性能劣化非常明显。原因在于竞争越激烈,排队的线程越多,公平锁引发的线程唤醒与上下文切换就越频繁。而非公平锁则能通过“幸存者偏差”,让那些原本就在运行的线程继续运行,从而维持了较高的处理效率。


🛡️⚠️ 第五章:场景选型——订单锁还是读写锁?

虽然非公平锁很快,但并不是万能的。

💣🕳️ 5.1 适用公平锁的场景:严格的时序性

在某些金融业务中,如果对请求的先后顺序有极致的要求(比如秒杀系统中极其严格的先到先得,虽然通常在 Redis 层解决,但在单机局部锁时也需注意),公平锁可以避免极端情况下的线程饿死。

💣🕳️ 5.2 适用非公平锁的场景:通用高并发

绝大多数的 Web 服务、中间件、数据库连接池,都应该首选非公平锁。因为在这种场景下,单次请求的公平性并不重要,系统的总吞吐量才是生命线。

🧬🧩 5.3 读写锁(ReentrantReadWriteLock)的补充

如果你面对的是“读多写少”的场景,比如配置信息的缓存,那么简单的ReentrantLock就不够用了。读写锁允许成百上千个线程同时读取,只有在写入时才互斥,这比非公平锁能带来更量级的性能飞跃。


🔄🧱 第六章:工程进阶——如何手写一个简单的“锁”?

为了真正理解ReentrantLock的精髓,我们需要模仿其核心逻辑,利用Unsafe类的 CAS 操作和线程阻塞工具LockSupport,手写一个迷你的锁实现。

其核心步骤如下:

  1. 定义一个state变量表示锁状态。
  2. 利用compareAndSwapInt尝试修改state
  3. 如果抢锁失败,将当前线程加入等待队列并调用LockSupport.park()
  4. 释放锁时,修改state并调用LockSupport.unpark()唤醒后继者。

💻🚀 实战代码:简易非公平锁实现
importjava.util.concurrent.atomic.AtomicInteger;importjava.util.concurrent.locks.LockSupport;importjava.util.concurrent.ConcurrentLinkedQueue;/** * 这是一个模仿 AQS 实现的简易非公平锁 */publicclassMiniReentrantLock{// 0: 自由, 1: 被占用privatefinalAtomicIntegerstate=newAtomicInteger(0);// 等待队列privatefinalConcurrentLinkedQueue<Thread>waiters=newConcurrentLinkedQueue<>();// 当前持有锁的线程privateThreadexclusiveOwnerThread;publicvoidlock(){// 非公平尝试:上来先抢一下if(state.compareAndSet(0,1)){exclusiveOwnerThread=Thread.currentThread();}else{// 抢不到,进队列waiters.add(Thread.currentThread());while(true){// 自旋并阻塞if(state.get()==0&&state.compareAndSet(0,1)){waiters.remove(Thread.currentThread());exclusiveOwnerThread=Thread.currentThread();return;}// 挂起线程,等待唤醒LockSupport.park();}}}publicvoidunlock(){if(Thread.currentThread()!=exclusiveOwnerThread){thrownewIllegalMonitorStateException();}exclusiveOwnerThread=null;state.set(0);// 唤醒队列中的第一个幸运儿Threadwaiter=waiters.poll();if(waiter!=null){LockSupport.unpark(waiter);}}}

🛡️⚠️ 第七章:避坑指南——重入锁的常见生产事故

💣🕳️ 7.1 unlock() 必须在 finally 中

这是初学者最容易犯的错。如果业务逻辑抛出异常而锁没有在finally中释放,该锁将永远被占用,导致全系统死锁。

💣🕳️ 7.2 锁的粒度问题

不要用一把大锁锁住整个复杂的业务流(涉及 RPC 调用、数据库事务)。这会导致所有的线程都在这把大锁上排队,非公平锁带来的那点优化也会被网络 IO 的延迟掩盖。正确的做法是:只在必要的数据修改处加锁。

💣🕳️ 7.3 条件变量(Condition)的使用

ReentrantLock配合Condition可以实现比synchronizedwait/notify更精细的唤醒逻辑。比如在一个阻塞队列中,我们可以精确唤醒“不满”的生产者或“不空”的消费者,避免无效的上下文切换。


🌍📈 第八章:总结——秩序与效率的权衡艺术

ReentrantLock的设计是 Java 并发工具类中最具代表性的“权衡艺术”。

  • 公平锁选择了秩序。它牺牲了 CPU 的局部性原理,换取了绝对的公平性。它适用于那些对响应时间敏感度一致、不希望有长尾延迟的系统。
  • 非公平锁选择了效率。它顺应了 CPU 的执行惯性,通过允许插队减少了上下文切换。它是绝大多数高并发、高吞吐场景下的默认选择。

理解这两种锁的底层差异,能让你在面临性能瓶颈时,通过一个简单的参数调整,就找回那“消失的 30% 吞吐量”。

结语:加锁的本质是让原本并行的程序变串行。既然变串行了,我们就要想方设法缩短串行的时间,并减少为了切换串行而付出的调度代价。这,就是并发编程优化的真谛。


🔥 觉得这篇深度解析对你有帮助?别忘了点赞、收藏、关注三连支持一下!
💬 互动话题:你在生产环境中使用 ReentrantLock 时,遇到过锁饥饿或者因为上下文切换导致的性能问题吗?欢迎在评论区分享你的实战经历!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 20:57:21

用Material Design In XAML Toolkit快速打造现代化WPF应用界面

用Material Design In XAML Toolkit快速打造现代化WPF应用界面 【免费下载链接】MaterialDesignInXamlToolkit Googles Material Design in XAML & WPF, for C# & VB.Net. 项目地址: https://gitcode.com/gh_mirrors/ma/MaterialDesignInXamlToolkit 还在为WPF…

作者头像 李华
网站建设 2026/5/13 1:28:55

如何用Dokploy实现全球化部署?5步搞定多语言界面

如何用Dokploy实现全球化部署&#xff1f;5步搞定多语言界面 【免费下载链接】dokploy Open Source Alternative to Vercel, Netlify and Heroku. 项目地址: https://gitcode.com/GitHub_Trending/do/dokploy 还在为海外用户的语言障碍头疼吗&#xff1f;担心不同地区的…

作者头像 李华
网站建设 2026/5/13 6:39:23

TensorRT INT8 量化难以维护?这套 CMake 工程化方案解决了

往期文章 RK3588+docker+YOLOv5部署:https://blog.csdn.net/FJN110/article/details/149673049 RK3588测试NPU和RKNN函数包装https://blog.csdn.net/FJN110/article/details/149669753 RK3588刷机:https://blog.csdn.net/FJN110/article/details/149669404 以及深度学习部署工…

作者头像 李华
网站建设 2026/5/14 21:04:41

推理速度大幅提升:Ubuntu + TensorRT 加速 YOLOv5

往期文章 RK3588+docker+YOLOv5部署:https://blog.csdn.net/FJN110/article/details/149673049 RK3588测试NPU和RKNN函数包装https://blog.csdn.net/FJN110/article/details/149669753 RK3588刷机:https://blog.csdn.net/FJN110/article/details/149669404 以及深度学习部署工…

作者头像 李华
网站建设 2026/5/12 15:24:44

YOLOv13 多尺度特征建模:PPM 空间金字塔池化模块解析

文章目录 PPM(Pyramid Pooling Module)模块原理与实现详解 1. 引言与背景 1.1 语义分割中的挑战 1.2 全局上下文的重要性 1.3 设计动机 2. PPM模块核心原理 2.1 金字塔池化概念 2.2 自适应池化机制 2.3 特征融合策略 3. 代码实现详解 3.1 模块初始化 3.2 前向传播过程 3.3 设…

作者头像 李华
网站建设 2026/5/12 14:53:00

Cap开源录屏工具:3步解锁专业级屏幕录制新体验

Cap开源录屏工具&#xff1a;3步解锁专业级屏幕录制新体验 【免费下载链接】Cap Effortless, instant screen sharing. Open-source and cross-platform. 项目地址: https://gitcode.com/GitHub_Trending/cap1/Cap 你是否曾经遇到过这样的场景&#xff1a;需要紧急录制一…

作者头像 李华