找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1561

积分

0

好友

231

主题
发表于 3 天前 | 查看: 9| 回复: 0

今天我们来深入探讨并发编程中一个核心且常被简单概括的概念——CAS(比较并交换,Compare-And-Swap)

谈及线程安全,很多人的第一反应是使用锁机制,例如 synchronizedReentrantLock。这确实是直观且普遍的做法。然而,还存在一种更为轻量级的思路:在不显式加锁的情况下,依然保障线程安全,这正是 CAS 技术的精妙之处。

值得注意的是,“无锁”这一说法具有一定迷惑性。CAS 并非彻底消除了锁,而是将同步的职责移交给了硬件层。本文将系统性地解析:CAS 的工作原理是什么?它如何确保线程安全?以及在实际应用中需要注意哪些问题?

一个经典案例:多线程累加为何出错?

假设我们需要统计服务器的访问次数,使用一个简单的整型变量 int count = 0;,并由多个线程同时执行 count++ 操作。

这行看似简单的代码背后隐藏着线程安全问题。count++ 并非原子操作,它实际上包含三个步骤:

  1. 读取变量 count 的当前值。
  2. 将读取的值加 1。
  3. 将计算结果写回 count 变量。

这三个步骤对应着 CPU 的多条指令,而操作系统调度器随时可能在任意指令之间切换线程。

举例说明:

  • 线程 A 读取到 count = 5,准备执行加1操作。
  • 此时发生线程切换,线程 B 开始执行,同样读取到 count = 5,将其加1后写回,此时 count 变为 6。
  • 线程 A 恢复执行,它持有的旧值仍是 5,加1后再次将 6 写回变量。

最终结果是两个线程各自完成了一次增加操作,但变量值只从 5 变为 6,仅增加了一次。这就是典型的线程不安全场景。

传统解决方案:加锁

最直接的解决方式是使用锁进行同步。

synchronized void increment() {
    count++;
}

通过这种方式,同一时刻仅允许一个线程进入该方法执行。即使发生线程切换,其他线程也需要等待锁被释放。

优点:逻辑清晰,可靠性高。
缺点:性能开销较大。线程阻塞、上下文切换以及用户态与内核态的切换都会带来额外的消耗。特别是在高并发但竞争并不激烈的场景下,锁本身可能成为性能瓶颈。

CAS 机制:基于“重试”的非阻塞同步

是否存在一种方法,既能避免线程排队等待,又能保证操作的安全性?答案是肯定的,这就是CAS

CAS 的核心思想非常直观:“我准备修改这个值,但前提是它现在的值和我上次看到时一样。”

具体而言,一个 CAS 操作涉及三个参数:

  • 内存地址 V:需要修改的变量。
  • 期望值 A:之前读取到的旧值。
  • 新值 B:希望更新成的目标值。

其执行逻辑是:
比较地址 V 处的当前值是否等于期望值 A。如果相等,则交换,将地址 V 的值更新为新值 B;否则,不执行任何操作(通常选择重试)。这个过程是由 CPU 提供的一条原子指令(例如 x86 架构下的 cmpxchg 指令)完成的,确保比较和交换两个动作作为一个不可分割的整体执行。

CAS工作原理示意图

使用 CAS 实现安全累加

仍以 count++ 为例,如何使用 CAS 实现?

AtomicInteger count = new AtomicInteger(0);
// 线程执行逻辑:
do {
    int current = count.get();      // 步骤1:读取当前值
    int next = current + 1;         // 步骤2:计算新值
} while (!count.compareAndSet(current, next)); // 步骤3:CAS尝试更新
  • 如果在此期间没有其他线程修改 count,那么 current 与内存中的当前值一致,CAS 操作成功,next 被写入。
  • 如果已有其他线程抢先修改了 count,导致 current 与内存当前值不符,则 CAS 操作失败,循环体将重新执行读取、计算并尝试交换的过程。

这个不断循环尝试的过程,就是常说的自旋(Spin)

CAS 真的完全“无锁”吗?

我们常称 CAS 为“无锁算法”,但严格来说,它只是没有在软件层面(如Java语言级别)使用传统的锁机制

在多核 CPU 环境下,多个核心可能同时访问同一内存地址。如果每个核心都能随意进行“读-改-写”操作,那么 CAS 的“比较并交换”过程就会受到干扰。因此,现代 CPU 在执行 CAS 指令时,会通过硬件机制来保证其原子性,主要方式有两种:

  1. 总线锁定:锁定整个内存总线,在此期间禁止其他 CPU 访问内存。这种方法简单但粗粒度,影响整体性能。
  2. 缓存锁定:利用缓存一致性协议(如 MESI 协议),只锁定目标变量所在的缓存行。这种方式效率更高,是现代 CPU 的常用实现。

因此,CAS 并非魔法,它高度依赖于底层硬件的支持。所谓“无锁”,更准确的理解是“无软件锁”,硬件层面的必要同步并未缺席。理解计算机底层原理,如 CPU缓存一致性协议与内存屏障,有助于更深刻地认识这一点。

客观看待 CAS 的优缺点

任何技术方案都是权衡的艺术,CAS 也不例外。

✅ 优势
  • 非阻塞:线程不会被挂起,避免了上下文切换的开销。
  • 高吞吐:在竞争程度较低的场景下,性能通常优于锁机制。
  • 灵活性:可作为基础原语,用于构建更复杂的无锁数据结构,例如 ConcurrentLinkedQueue
❌ 劣势
  • ABA 问题:变量值经历了 A -> B -> A 的变化,CAS 在比较时会误以为值未发生变化。可通过 AtomicStampedReference(携带版本戳的引用)或 AtomicMarkableReference 来解决。
  • 自旋开销:在高竞争场景下,大量线程可能长时间循环重试,导致 CPU 资源空转,反而降低系统性能。
  • 功能局限:只能保证对单个共享变量的原子操作。如果需要同时原子性地更新多个相关联的变量,CAS 无法直接实现,仍需借助锁或其他同步机制。

选择 CAS 还是锁,需根据具体场景判断。对于“读多写少”、“冲突概率低”的场景(如计数器、状态标志位),CAS 非常合适。但对于竞争激烈的资源池管理或队列头尾指针频繁变更等场景,锁可能是更稳妥的选择。

在 Java 中的应用

自 JDK 1.5 起,java.util.concurrent.atomic 包提供了一系列原子类,如 AtomicIntegerAtomicLongAtomicReference 等。这些类的底层实现均通过 sun.misc.Unsafe 类调用本地 CAS 指令。

例如,AtomicInteger.incrementAndGet() 方法的核心逻辑就是我们上面演示的自旋 CAS 循环。此外,像 ConcurrentHashMap 这类核心 数据库与中间件 在 JDK 8 及之后的版本中,对桶(Bucket)内节点的插入采用了 CAS + synchronized 的混合策略:首先尝试使用 CAS 更新,若失败(表明存在竞争),再针对链表头或树根节点使用 synchronized 进行加锁。这种设计巧妙地平衡了性能与安全性。

总结

CAS 并非并发编程的“银弹”,但它提供了一种更细粒度、更高性能的同步控制思路。深入理解其工作原理、优势与局限,能够帮助开发者在设计高并发系统时做出更合适的技术选型,在性能与安全性之间找到最佳平衡点。下次当有人提起“CAS是无锁的”,我们可以更全面地理解:它实质上是将同步的复杂度转移到了硬件层面。




上一篇:嵌入式异步日志模块设计与实现:从printf到Crash Dump的完整方案
下一篇:StarRocks与数据湖架构深度解析:湖仓一体实践与成本优化方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 22:56 , Processed in 0.235818 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表