恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

深入解析CAS与自旋锁:高并发场景下的无锁编程利器

  • 首页
  • 资讯中心
  • /
  • 深入解析CAS与自旋锁:高并发场景下的无锁编程利器

相关资讯

如何快速重置AnyDesk ID?Windows一键生成全新ID的完整实战指南 2026/8/15 4:16:39
Windows下MySQL自定义路径安装与配置全攻略 2026/8/15 4:16:39
五四红旗团委线上投票系统全流程设计与技术实现指南 2026/8/15 4:16:39

最新资讯

PHP中CSRF攻击防御实战指南
Claude Code CLI 远程执行架构解析:Bridge与Remote Control模块设计
APMCM数学建模竞赛:从解题到建模的实战指南与团队协作策略
编译原理期末总复习:从词法分析到代码生成的完整知识重构
IntelliJ IDEA集成google-java-format实现保存自动格式化
LangChain实战:从零构建大模型应用,打通工具调用与RAG知识库

今日推荐

内景 空间站内部 中国空间站 太空 内仓
重新定义数据接口:3个突破性场景让通达信数据读取更智能
5大网络安全实操平台,免费练手入门,轻松掌握攻防技能

本周热门

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁
如何快速生成中国车牌图片:Python开源工具完整指南
当 LLM 遇见大文档:主流开源项目如何处理上下文超限

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

深入解析CAS与自旋锁:高并发场景下的无锁编程利器

发布时间:2026/8/15 4:16:39
深入解析CAS与自旋锁:高并发场景下的无锁编程利器 1. 从一次诡异的并发计数错误说起那天下午我盯着监控面板上一个持续跳动的计数器心里咯噔一下。这是一个简单的用户在线状态统计服务逻辑清晰用户上线时计数器加一下线时减一。理论上在任意时刻这个数字都应该等于真实的在线用户数。但监控显示这个数字偶尔会“漂移”——在没有任何用户上下线操作的时间窗口内它自己会莫名其妙地减少几个过一会儿又恢复。这显然不是网络延迟或数据上报丢失能解释的因为丢失应该是单向的减少而这种“漂移”更像是…数据被覆盖了。我立刻把怀疑的目光投向了负责这个计数更新的方法。代码很简单就一行count count 1;或者count count - 1;。在单线程世界里这行代码坚如磐石。但在我们这个每秒处理数万次请求的高并发服务里它就成了薛定谔的猫——你永远不知道执行这一刻的count值是不是已经被其他线程偷偷改掉了。这就是典型的竞态条件多个线程在没有适当同步的情况下同时读写共享变量导致最终结果依赖于线程执行的精确时序而这个时序是不可预测的。为了解决这个问题我最初的想法是加锁。用synchronized关键字把读写操作锁起来或者用ReentrantLock。这确实能解决问题保证同一时刻只有一个线程能操作count。但性能测试结果让人皱眉在高并发场景下锁带来的线程挂起、上下文切换开销让整个服务的吞吐量下降了近30%。对于一个核心状态服务来说这个代价有点大。就在我纠结于锁的性能损耗时团队里的老张走过来看了一眼说“试试AtomicInteger的incrementAndGet()吧底层用的是 CAS没锁性能好。” 这是我第一次在工作中被直接点明去用 CAS。AtomicInteger我倒是用过知道它是线程安全的但对其底层原理compareAndSet比较并交换也就是 CAS以及与之相关的“自旋”概念当时只有一个模糊的印象。这次诡异的计数错误逼着我必须把 CAS 和自旋锁彻底搞明白。这不仅是为了修复一个 Bug更是为了在日后设计高性能并发程序时手里能多一件称手的兵器。2. CAS 的核心思想乐观的并发控制哲学要理解 CAS首先要跳出“锁”的思维定式。传统的互斥锁如synchronized是一种悲观锁。它悲观地认为只要有多线程访问共享数据就一定会发生冲突。所以它采取了一种保守策略在我访问数据之前先加锁把其他所有线程都挡在外面等我操作完了释放锁你们再进来。这就像只有一个洗手间的办公室一个人进去就锁门其他人只能在门口排队等着。而 CAS 代表的是一种乐观锁的哲学。它乐观地认为线程间不总是会发生冲突。所以它在操作数据时不会先去获取锁而是直接去尝试修改。但为了安全它在修改前会做一次检查我认为现在这个变量的值应该是 A如果它真的是 A那我就把它改成 B如果我发现它已经不是 A 了说明在我“认为”和“修改”之间的极短间隙里有其他线程改动了它那么我这次修改就失败不会执行。这个过程是原子的不可被中断。这个“检查-设置”的操作就是compareAndSet。它的工作流程可以抽象为以下伪代码boolean compareAndSet(预期值 expectedValue, 新值 newValue) { if (当前内存值 expectedValue) { 当前内存值 newValue; return true; // 成功 } else { return false; // 失败 } }关键在于整个if判断和赋值操作是一条不可分割的 CPU 指令。在现代多核处理器上这条指令通常对应着CMPXCHGCompare and Exchange之类的汇编指令由硬件保证其原子性。这就从最底层杜绝了“检查通过后赋值前值被其他线程修改”的可能。举个例子我们回到那个计数器问题。使用AtomicInteger的incrementAndGet()其内部实现大致如下public final int incrementAndGet() { for (;;) { // 这是一个自旋循环 int current get(); // 获取当前值比如是5 int next current 1; // 计算期望的下一个值6 if (compareAndSet(current, next)) { // 核心如果当前值还是5就设置为6 return next; // 成功返回6 } // 如果失败说明current已经不是5了比如被其他线程改成了8循环重试 } }你会发现CAS 操作本身是原子的、快速的但它处理竞争失败的方式是重试。线程不会被挂起而是立刻在原地循环再次读取当前值重新计算然后发起新一轮的 CAS。这种通过循环不断尝试的模式就是“自旋”的由来。而基于 CAS 构建的、通过循环重试来实现互斥的锁就被称为自旋锁。注意CAS 是一种底层原语而自旋锁是利用 CAS 原语实现的一种锁策略。我们常说的“CAS 自旋锁”指的是这种实现方式。3. 自旋锁的实战自己实现一个简易版本理解了 CAS 的思想后我们完全可以自己动手利用 Java 提供的AtomicReference或Unsafe类后者不推荐直接使用来实现一个最简单的自旋锁。这能让我们对“自旋”有更切身的体会。下面是一个名为SimpleSpinLock的极简实现import java.util.concurrent.atomic.AtomicReference; public class SimpleSpinLock { // 使用AtomicReference来持有锁的“所有者”初始值为null表示锁空闲 private AtomicReferenceThread owner new AtomicReference(); // 加锁 public void lock() { Thread currentThread Thread.currentThread(); // 关键通过CAS尝试将owner从null设置为当前线程 // 如果失败owner不是null说明锁被其他线程持有就循环重试自旋 while (!owner.compareAndSet(null, currentThread)) { // 空循环即“自旋”。在实际中可能会加入一些优化如Thread.yield() // 但纯空循环在高竞争下会浪费大量CPU } System.out.println(currentThread.getName() 获取了锁); } // 解锁 public void unlock() { Thread currentThread Thread.currentThread(); // 解锁时通过CAS将owner从当前线程设置回null // 只有锁的持有者才能解锁这里简化了检查 if (owner.compareAndSet(currentThread, null)) { System.out.println(currentThread.getName() 释放了锁); } else { // 非持有者尝试解锁这里可以抛出异常 throw new IllegalMonitorStateException(Attempt to unlock without holding the lock); } } }我们来写个测试用例模拟两个线程竞争这个锁public class SpinLockTest { private static int counter 0; private static SimpleSpinLock lock new SimpleSpinLock(); public static void main(String[] args) throws InterruptedException { Runnable task () - { for (int i 0; i 10000; i) { lock.lock(); // 获取自旋锁 try { counter; // 临界区操作 } finally { lock.unlock(); // 释放锁 } } }; Thread t1 new Thread(task, Thread-1); Thread t2 new Thread(task, Thread-2); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(Final counter value: counter); // 正确输出 20000 } }运行这个程序你会看到控制台交替打印“获取了锁”和“释放了锁”的信息并且最终的counter值一定是 20000证明了我们自旋锁的有效性。自己实现带来的思考CPU 资源消耗在lock()方法的while循环中如果锁被其他线程长期持有当前线程就会一直空转白白消耗 CPU 周期。这是自旋锁最显著的缺点。它适用于锁持有时间非常短的场景此时线程自旋等待的代价低于被挂起再唤醒的上下文切换开销。公平性问题我们的SimpleSpinLock是非公平锁。在锁释放的瞬间哪个线程恰好执行到 CAS 操作哪个就能抢到锁不讲究先来后到。在高竞争下可能导致某些线程“饥饿”。可重入性我们的锁是不可重入的。如果一个线程已经持有锁再次调用lock()会因为owner不是null而是它自己而导致 CAS 失败陷入死循环。这就是“重入”死锁。ReentrantLock是可重入的内部维护了一个持有计数。通过这个简单的实现我们清晰地看到了自旋锁的骨骼一个共享的状态变量owner一个 CAS 操作来竞争这个状态以及一个循环来应对竞争失败。Java 中AtomicInteger等原子类的递增操作其内部就包含了这样的自旋逻辑。而更复杂的锁如ReentrantLock在其非公平模式下的首次加锁尝试也是基于类似的 CAS 操作。4. CAS 的“阿喀琉斯之踵”你必须知道的三大问题CAS 并非银弹它在带来高性能的同时也引入了三个经典问题。理解这些问题是正确使用 CAS 和自旋锁的前提。4.1 ABA 问题你看到的“没变”可能已经沧海桑田这是 CAS 最著名的一个陷阱。假设共享变量V的初始值是 A。线程1读取V为 A。线程1被挂起。线程2将V从 A 改为 B。线程3又将V从 B 改回了 A。线程1恢复运行执行 CAS(V, 预期值A, 新值C)。此时它检查V的值发现确实是 A于是 CAS 成功将 A 改为了 C。从线程1的视角看V的值没变过一直是A所以我的修改是合理的。但事实上V已经经历了一个 A-B-A 的变化过程。这个中间状态 B 可能蕴含着重要的业务信息。例如V是一个链表的头节点引用。A 是头节点线程1想用 CAS 把 A 替换成 C。在线程1挂起期间线程2移除了 A插入了 B然后线程3又移除了 B而 A 恰好被垃圾回收后另一个新对象复用了这块内存地址或者就是原来的 A 对象被重新插入。对于线程1的 CAS 来说它成功了但这可能破坏链表的结构。解决方案版本号/时间戳最常见的解决方案。不单纯比较值而是比较“值版本号”。每次变量被更新版本号都递增。Java 中的AtomicStampedReference就是为此而生。它维护了一个Pair对象包含引用和一个int类型的版本戳stamp。AtomicStampedReferenceNode headRef new AtomicStampedReference(nodeA, 0); // 更新时需要同时提供预期的引用和预期的版本戳 boolean success headRef.compareAndSet(nodeA, nodeC, 0, 1);在上面的链表例子中即使引用从 A 变回 A版本戳也从 0 变成了 2CAS 就会失败。布尔标记有些场景可以用一个额外的布尔标记位来表示对象是否已被修改过。4.2 循环时间长带来的开销正如我们实现自旋锁时看到的如果竞争激烈线程可能长时间循环空转大量消耗 CPU 资源。虽然自旋避免了上下文切换的开销但把 CPU 周期浪费在无意义的循环上同样会降低系统的整体吞吐量。解决方案自适应自旋JVM 内部的锁优化如 synchronized 的轻量级锁和ReentrantLock的实现中就采用了自适应自旋。简单说如果线程最近刚刚成功获得过锁JVM 会认为这次自旋也很可能成功就允许它多自旋一会儿。反之如果很少成功就可能直接挂起线程。这是一种基于历史经验的启发式优化。手动退让在自旋循环中可以适时调用Thread.yield()提示调度器让出 CPU或者短暂睡眠Thread.sleep(1)但这需要谨慎控制否则可能增加延迟。升级为阻塞锁当自旋超过一定阈值后直接升级为传统的阻塞等待。ReentrantLock在尝试 CAS 获取锁失败后就会将线程放入等待队列并挂起。4.3 只能保证一个共享变量的原子操作CAS 指令本身是针对一个内存地址进行操作的。如果你想同时原子地更新两个独立的变量比如i和j一个 CAS 指令办不到。解决方案封装成对象将多个需要原子更新的变量封装到一个不可变对象里然后使用AtomicReference来更新这个对象引用。这实际上是把对多个变量的操作转化为对一个引用变量的 CAS 操作。public class Point { public final int x, y; // 不可变 public Point(int x, int y) { this.x x; this.y y;} } AtomicReferencePoint pointRef new AtomicReference(new Point(0, 0)); // 原子地同时更新x和y Point oldP pointRef.get(); Point newP new Point(oldP.x 1, oldP.y 1); while (!pointRef.compareAndSet(oldP, newP)) { oldP pointRef.get(); newP new Point(oldP.x 1, oldP.y 1); }使用锁如果逻辑非常复杂或者涉及多个完全不相关的共享资源使用互斥锁往往是更清晰、更安全的选择。不要为了用 CAS 而用 CAS。5. 场景抉择CAS/自旋锁 vs ReentrantLock vs synchronized现在我们对 CAS 和自旋锁有了深入理解再回头看ReentrantLock和synchronized就能更清晰地做出技术选型。它们之间的关系和区别可以用一个简单的表格来概括特性CAS / 自旋锁 (如AtomicInteger)ReentrantLock(默认非公平)synchronized实现机制乐观锁硬件指令CMPXCHG保证原子性失败则自旋重试。内部基于AbstractQueuedSynchronizer (AQS)首次尝试使用 CAS失败后入队阻塞。JVM 内置监视器锁通过monitorenter/monitorexit字节码实现。锁粒度无锁仅针对特定变量或细粒度锁自旋锁。显式锁粒度由开发者控制。隐式锁粒度是对象或类。性能特点极低延迟无上下文切换适合锁持有时间极短、竞争不激烈的场景。自旋消耗 CPU。灵活可尝试获取、可定时、可中断。在高竞争、锁持有时间长时性能通常优于synchronized因优化策略更灵活。早期性能较差但经过 JVM 大量优化偏向锁、轻量级锁、自旋适应、锁粗化、消除等在大多数常见场景下性能已非常好且仍在持续优化。功能特性仅提供原子更新功能单一。自旋锁需自己实现高级功能。功能丰富可重入、可中断、可超时、可设置公平/非公平、支持多个条件变量。功能简单可重入、非公平、不可中断、不可超时、单个隐式条件变量wait/notify。编程复杂度中高。需要开发者深刻理解内存可见性、ABA 问题等正确实现有难度。中。需要显式地lock()和unlock()必须在finally中易出错。低。语法简洁自动释放锁不易出错。适用场景1. 简单的计数器、状态标志位更新。2. 实现无锁数据结构如并发队列。3. 作为更高级同步器如ReentrantLock的基础构件。1. 需要高级功能如可中断、超时、公平性。2. 竞争激烈且锁持有时间较长的场景。3. 需要多个等待条件Condition。1. 大多数常规的同步场景。2. 追求代码简洁和开发效率。3. JVM 能很好优化的场景。如何选择一个实用的决策流你的需求是否只是一个简单的原子整数/引用更新如果是直接用AtomicInteger、AtomicLong、AtomicReference。这是最轻量、最快的选择。你需要可中断、超时、公平锁、多个条件变量这些高级功能吗如果需要选ReentrantLock。以上都不是只是一个普通的临界区需要保护优先考虑synchronized。理由如下开发友好语法简单不易漏写解锁。持续优化它是 Java 语言的一部分享受 JVM 团队最高优先级的优化。在 JDK 不断迭代中其性能差距与ReentrantLock在很多场景下已微乎其微。未来可期随着 Project Loom 的推进虚拟线程synchronized与虚拟线程的协作可能更自然。足够好用对于 90% 的并发控制场景synchronized的功能已经足够。关于“自旋”的误区澄清很多人认为synchronized是“重量级锁”一上来就挂起线程。这是过时的认知。现代 JVM 中synchronized的获取也经历了“偏向锁-轻量级锁自旋-重量级锁”的锁升级过程。当竞争不激烈时它也会先尝试自旋轻量级锁失败后才真正挂起线程膨胀为重量级锁。也就是说synchronized内部也使用了自旋优化。所以单纯因为“自旋更快”而选择自己实现自旋锁或ReentrantLock在很多情况下可能并没有优势反而增加了复杂度。6. 从理论到实践一个真实场景的优化案例让我们回到文章开头的那个在线计数器问题。最初版本是count存在竞态条件。我们分析了三种改进方案方案一使用synchronizedprivate int count 0; public synchronized void userOnline() { count; } public synchronized void userOffline() { count--; } public synchronized int getOnlineCount() { return count; }优点简单安全代码清晰。缺点所有增减和读取操作都需要获取同一把锁在高并发读getOnlineCount调用频繁时会产生不必要的竞争影响吞吐量。方案二使用ReentrantLockprivate int count 0; private final ReentrantLock lock new ReentrantLock(); public void userOnline() { lock.lock(); try { count; } finally { lock.unlock(); } } // ... 类似实现 offline 和 get优点和synchronized效果类似但我们可以选择公平锁或者尝试非阻塞获取锁tryLock。缺点同样存在读写竞争问题且代码更冗长。方案三使用AtomicInteger(CAS)private AtomicInteger count new AtomicInteger(0); public void userOnline() { count.incrementAndGet(); } public void userOffline() { count.decrementAndGet(); } public int getOnlineCount() { return count.get(); }优点极致性能incrementAndGet()内部是 CAS 自旋无锁读写操作 (get) 也完全无竞争性能最高。代码简洁和原始代码几乎一样简洁。完全解决竞态原子操作保证安全。缺点ABA 问题在这个特定场景下ABA 问题有影响吗计数器从 5 变成 6 再变回 5对于“当前在线人数”这个语义来说没有影响。我们只关心最终值不关心中间过程。所以 ABA 在这里不是问题。自旋开销在超高并发、极端竞争下大量线程同时自旋尝试增加计数器会导致 CPU 使用率飙升。但考虑到“用户上下线”这个操作频率相对于 CPU 周期来说并不算极端密集且AtomicInteger的自旋实现非常高效这个开销通常是可接受的。最终选择与实测我们选择了方案三。原因很直接这个场景完美契合 CAS 的优势——操作简单增减1、竞争时间极短、不关心 ABA 问题。部署上线后那个诡异的计数“漂移”现象彻底消失服务的 CPU 使用率和吞吐量相比最初的错误版本和加锁版本都有显著改善。AtomicInteger成为了这个计数器场景下的最优解。这个案例告诉我们技术选型没有绝对的好坏只有是否适合。CAS 和自旋锁是并发工具箱里一把锋利的手术刀在合适的场景下短临界区、低竞争度、简单原子操作它能以最小的开销解决同步问题。但在错误的场景下长临界区、高竞争、复杂操作它可能会带来更糟的性能和更复杂的 Bug。理解其原理和边界才能做出最明智的选择。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号