自旋锁到底在“转”什么?从抢锁到 CPU 空转,讲透原理与适用场景

发布时间:2026/10/17 10:49:08

自旋锁到底在“转”什么?从抢锁到 CPU 空转,讲透原理与适用场景 平时说到锁很多人脑子里第一反应都是没抢到锁那就等着。但自旋锁不一样。它的思路有点“轴”锁现在被别人拿着没关系我不睡我就在这儿一直试直到你把锁放出来。这就是“自旋”两个字的由来。听起来似乎很浪费 CPU但它偏偏又大量存在于操作系统、JVM、并发框架和底层组件里。原因也不复杂有时候等一会儿比睡一觉再醒来更划算。一、先用一个很简单的例子理解自旋假设公司只有一台打印机。小王正在打印一张纸你也要打印。这时候有两种做法。第一种发现打印机被占用 ↓ 回工位坐下 ↓ 等别人通知 ↓ 再走回来打印这很像普通的阻塞等待。另一种打印机被占用 ↓ 站旁边等几秒 ↓ 看看好了没 ↓ 没好再看一次 ↓ 好了马上接着用这就是自旋锁的感觉。区别就在于阻塞锁会让当前线程睡眠自旋锁不会它会一直尝试拿锁。二、自旋锁底层到底在做什么最简单的自旋锁可以先写成这种伪代码while (lock 1) { // 什么都不干一直检查 } lock 1; // 临界区代码 lock 0;意思非常直接lock 0代表锁空闲。lock 1代表锁已经被占用。线程来抢锁时如果发现lock 1就一直循环。这就是所谓busy waiting忙等。不过上面这段代码实际上是不安全的。因为两个线程可能同时看到lock 0然后一起执行lock 1这样两个人就同时进临界区了锁直接失效。所以真正的自旋锁必须依赖一种非常重要的 CPU 原子操作CAS也就是Compare And Swap比较并交换。三、CAS 才是自旋锁真正的底座CAS 可以简单理解成一句话如果这个值现在还是我预期的值就把它改掉如果已经被别人改了那我什么都不做。比如当前 lock 0线程 A 想拿锁。它执行CAS(lock, 0, 1)意思是如果 lock 现在还是 0 就改成 1如果成功线程A拿到锁如果失败说明别人先拿走了于是继续尝试。代码大概可以理解成while (!CAS(lock, 0, 1)) { // 自旋 }释放锁lock 0;所以一个最简化的自旋锁本质其实就是CAS ↓ 失败 ↓ 继续 CAS ↓ 失败 ↓ 继续 CAS ↓ 成功 ↓ 进入临界区这就是所谓的“自旋”。四、为什么不直接让线程睡觉问题就来了。既然没抢到锁那把线程挂起不就好了为什么还要浪费 CPU 一直循环关键在于线程睡眠和唤醒其实并不是免费的。假设线程 A 没拿到锁。如果使用阻塞锁可能经历线程A运行 ↓ 发现锁被占用 ↓ 线程进入阻塞状态 ↓ 操作系统调度其他线程 ↓ 保存线程A上下文 ↓ 过一会锁释放 ↓ 重新唤醒线程A ↓ 恢复线程A上下文 ↓ 继续运行这里涉及一个重要概念上下文切换。CPU 要保存和恢复寄存器程序计数器栈信息线程状态这些操作都有成本。如果对方只占锁几百纳秒或者几微秒那你刚把线程挂起来锁可能已经释放了。结果变成为了等一会儿反而付出了一次完整的线程切换成本。这就像你去厕所发现有人“里面有人。”如果对方还有 5 秒就出来你没必要坐电梯回办公室 ↓ 坐下 ↓ 再坐电梯回来站门口等一下反而更快。这正是自旋锁存在的意义。五、自旋锁真正赌的是什么自旋锁其实是在赌一件事我赌你很快就会把锁释放。如果赌对了线程A持锁 ↓ 线程B自旋几次 ↓ 线程A释放 ↓ 线程B立刻拿到非常划算。因为线程 B 根本没有发生阻塞和唤醒。但如果赌错了线程A持锁10秒线程 B 还在那里CAS CAS CAS CAS CAS CAS ……那就惨了。CPU 可能被它白白占满。所以自旋锁有一个非常明显的特点适合锁持有时间非常短的场景。六、为什么自旋锁特别适合多核 CPU假设只有一个 CPU 核心。线程 A 拿着锁A持有锁线程 B 开始自旋B疯狂检查锁问题来了。CPU 此时正在执行 B。那 A 怎么运行A 不运行就没办法释放锁。于是很可能出现B在等A释放锁 但B一直占着CPU A反而得不到运行机会所以在单核环境里自旋通常意义不大。但多核就不一样。比如CPU Core 1 → 线程A → 正在使用锁 CPU Core 2 → 线程B → 正在自旋这时候A继续执行 ↓ 很快释放锁与此同时B一直观察 ↓ 立刻拿到锁这才是自旋锁最理想的状态。所以自旋锁天然和多核 CPU关系非常密切。七、自旋锁最大的缺点它真的会烧 CPU假设线程A拿锁以后 执行了5秒线程 Bwhile (!CAS(...)) { }这 5 秒里它并不是睡着。而是在判断 失败 判断 失败 判断 失败 ……CPU 使用率可能直接上去。如果只有一个线程还好。如果100个线程同时竞争一把锁线程1 → 自旋 线程2 → 自旋 线程3 → 自旋 …… 线程100 → 自旋所有人一起抢 抢 抢 抢 抢CPU 就会花大量时间在无意义竞争上。所以自旋锁不是“比阻塞锁高级”而是适用于不同场景。八、实际系统一般不会让线程无限自旋工程里通常不会真的写while (true) { CAS... }然后一直转到天荒地老。更常见的策略是先自旋 ↓ 尝试几次 ↓ 还是失败 ↓ 再阻塞比如尝试1次 失败 尝试2次 失败 尝试3次 失败 ……达到一定次数以后算了不等了 ↓ 线程挂起这样就把两种方案结合起来了短时间等待 → 自旋 长时间等待 → 阻塞这其实比“永远自旋”聪明得多。九、自旋次数应该固定吗也不一定。更聪明的实现甚至会根据之前的情况调整。比如某个锁以前经常自旋3次左右就拿到了那系统可能觉得这个锁一般释放得挺快可以多等一会儿。但如果过去经常自旋半天都拿不到系统可能就会别浪费 CPU 了早点挂起吧。这种思路通常叫自适应自旋。核心意思就是自旋多久不一定固定而是根据运行情况动态决定。十、自旋锁和互斥锁到底怎么选可以把它们想成两个完全不同的等待策略。自旋锁没抢到我就在门口等特点不睡眠 不主动让出 CPU 响应快 但浪费 CPU适合临界区非常短 锁很快释放 竞争不是特别激烈 多核 CPU阻塞锁没抢到我先回去睡一会 有位置再叫我特点节省 CPU 但涉及阻塞和唤醒 可能有上下文切换成本适合锁可能持有较长时间 竞争激烈 等待时间不可预测所以两者并不存在谁绝对更快。真正决定性能的是等待时间 竞争程度 CPU核心数量 临界区长度十一、Java 里其实到处都有“自旋思想”Java 开发里可能很少自己手写一个SpinLock但自旋思想其实非常常见。比如AtomicInteger AtomicLong AtomicReference这些原子类大量依赖CAS典型逻辑就是读取旧值 ↓ 计算新值 ↓ CAS尝试更新 ↓ 失败 ↓ 重新读取 ↓ 再次CAS本质上就是一种CAS 重试。再比如很多高性能并发组件也会使用类似思路先快速尝试 ↓ 短暂自旋 ↓ 竞争严重后再阻塞因为真正高性能的并发设计通常不是简单选择一直自旋或者直接阻塞而是在两者之间找平衡。十二、还有一个细节自旋时最好别真的“死转”最粗暴的写法while (locked) { }虽然能转但并不友好。现代 CPU 和运行时通常会提供一些“提示”。Java 里有Thread.onSpinWait();它相当于告诉 CPU我现在正在自旋等待不是在做正常计算。底层可以利用类似PAUSE这样的 CPU 指令优化自旋行为。好处包括减少资源消耗 降低流水线压力 改善超线程环境表现所以真正的高性能自旋并不是傻乎乎地疯狂 while。而是尽量让 CPU 知道这个线程只是在短暂等待。十三、一个例子把自旋锁彻底串起来假设现在有两个线程。线程 A拿到锁 ↓ 修改一个计数器 ↓ 只需要几十纳秒 ↓ 释放锁线程 B 此时过来。如果采用阻塞发现锁被占 ↓ 线程挂起 ↓ 操作系统调度 ↓ A释放锁 ↓ 重新唤醒B ↓ B继续运行有可能线程切换的成本比真正等待锁的时间还大。换成自旋B第一次CAS → 失败 B第二次CAS → 失败 B第三次CAS → 成功整个过程可能很快就结束了。这里自旋非常划算。但如果 A 拿锁以后请求数据库 ↓ 调用远程接口 ↓ 睡眠3秒B还在那里自旋CAS CAS CAS CAS ……那就是典型的错误使用。所以判断自旋锁适不适合只需要问一个问题拿锁的人到底多久能出来写在最后自旋锁看起来像是一个很高级的并发概念其实它的思想特别直接锁暂时拿不到但我觉得你马上就会释放所以我先不睡原地再试几次。它的底层核心可以压缩成CAS ↓ 成功 → 拿锁 失败 ↓ 继续 CAS ↓ 继续尝试而它存在的真正原因是线程阻塞 线程唤醒 上下文切换本身也是有成本的。所以当等待锁的时间 线程挂起和重新调度的成本自旋往往更划算。但反过来如果锁长期不释放自旋 拿 CPU 时间不停做无用功这时候阻塞反而更合理。所以理解自旋锁以后会发现它背后其实是一种很典型的性能优化思想不要一发现资源不可用就立刻进入重量级等待而是先赌一次——它也许马上就好了。赌对了省掉一次昂贵的线程切换。赌错了就得及时收手。这也是为什么真正成熟的并发实现通常不是“只自旋”或者“只阻塞”而是在两者之间动态取舍。
延伸阅读

更多相关文章

2026/10/17 10:48:46

Excel行列求和:两组快捷键搞定全表与跳空汇总

​在日常处理Excel报表时,行列求和恐怕是出现频率最高的操作之一。很多人习惯用鼠标点击工具栏上的“自动求和”按钮,或者手工输入SUM函数再拖动填充,虽然也能得出结果,但效率总差那么点意思。其实,Excel内置了一组堪称…

2026/10/16 5:26:31

力扣高频SQL50题知识点

一、查询select product_id from Products where low_fatsY and recyclableY;select 输出 from 表名where 条件;select name from Customer where referee_id ! 2 or referee_id is NULL;对于条件为某一元素为NULL用“元素 is NULL”select name,population,area…

2026/10/16 2:36:07

day24_jsvmp后两个生成算法

一、在switch中插桩,先看大概日志信息有什么变化! “case”,a[F1],“stack”,Y看见array一直递增!数组变化【视频35分】 我们就知道大概要插桩什么了,比如chartCodeAt,String.FromChartCode,parseInt等等操…

2026/10/17 10:45:15

子母钟系统采购——应以全生命周期总拥有成本为核心标尺

关键词:数字子母钟、gps子母钟系统、ntp子母钟、网络子母钟系统医院、轨道交通、学校、工厂、智慧楼宇等项目建设子母钟系统时,采购常陷入只对比硬件报价、优先低价方案的误区。前期投入看似低廉,但运行3-5年后易出现备件停产、维…

2026/10/17 10:45:15

SQL生成落地关键:定制能力与推理稳定性实战指南

1. 这不是选“谁家大模型更好”,而是选“谁能把SQL生成这件事真正落地”最近两周,我连续帮三支不同行业的团队做过SQL生成场景的技术评估:一支是金融风控中台,要从千万级客户行为日志里实时提取异常路径;一支是电商BI小…

2026/10/17 10:45:15

从“人眼排雷”到“智能防控”:AI文档审核如何赋能企业提效?

在政企单位与大型企业的日常运转中,文档不仅是信息的载体,更是法律效力的延伸。从对外发布的红头文件、商务合同,到对内的管理制度、会议纪要,任何一处错别字、标点误用,甚至语义歧义,都可能引发严重的合规…

2026/10/17 10:45:15

鸿蒙Flutter Scrollbar实战:适配触摸与鼠标双模式的滚动条调优

说实话,写鸿蒙生态下的Flutter组件,每次都有种“带着熟悉武器进新战场”的感觉。Scrollbar这个组件,在标准Flutter里你可能只用一行Scrollbar(child: ListView(...))就完事了,但在OpenHarmony上,它偏偏是“细节决定体验…

2026/10/17 10:45:15

Agent学习总结二

二、智能体发展史2.1、基于符号与逻辑的早期智能体人工智能领域的早期探索,深受数理逻辑和计算机科学基本原理的影响。当时候研究者们普遍相信:人类的智能,尤其是逻辑推理能力,可以被形式化的符号体系所捕捉和复现。这一核心思想催…

2026/10/17 10:40:15

机械臂预分析:用Matlab可视化可达空间与可操作性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑