ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java并发编程之CAS

Java并发编程之CAS

一、 概念:什么是 CAS?

CAS(比较并交换)是一种无锁并发的原子操作策略(属于乐观锁)。
它包含三个操作数:

  • V (Value):内存中的实际当前值。

  • A (Expected):预期原值(旧值)。

  • B (New):准备更新的新值。

核心逻辑:当且仅当内存值V等于预期值A时,才将内存值修改为B。否则,说明值已经被其他线程改过了,本次操作失败。整个过程是一个不可分割的原子操作。


二、 底层实现:从 Java 到 CPU 指令

CAS 在 Java 中的实现是一条自顶向下的调用链:

  1. Java 应用层:我们通常使用AtomicInteger等原子类,或者ReentrantLock的底层。

  2. Java API 层:调用sun.misc.Unsafe类提供的native方法,如compareAndSwapInt。

  3. JNI 桥接层(unsafe.cpp):

    • 通过对象引用和内存偏移量(Offset)计算出属性的绝对物理地址。

    • 将值交给底层原子操作方法Atomic::cmpxchg。

  4. 硬件指令层:在 x86 架构下,最终编译为带有lock前缀的cmpxchg指令。

    • 原子性:lock前缀会锁定缓存行(或总线),保证“读-比较-写”的硬件级别原子性。

    • 可见性/有序性:lock前缀自带全内存屏障(Memory Barrier)效果,强制刷新主存,并禁止指令重排序。


三、 优点与缺点

优点:

  • 无锁并发,性能极高:无需加锁,不涉及操作系统的互斥量(Mutex),避免了用户态与内核态的上下文切换开销。

  • 非阻塞:如果 CAS 失败,线程不会像synchronized那样被挂起阻塞,而是可以继续尝试(自旋),或者去做别的事情。

缺点:

  • 只能保证一个变量的原子操作:对于多个变量同时修改,CAS 无能为力(可以使用AtomicReference封装为一个对象,或者干脆加锁)。

  • ABA 问题(详见下文)。


四、 存在的问题及解决方案(核心考点)

CAS 虽然好,但它天生存在三大“隐患”:

1. 自旋开销大 (CPU 开销)
  • 问题:如果 CAS 一直失败(高并发场景),Java 层的do-while循环(自旋)会一直占用 CPU 时间片空转。如果长时间不成功,会给 CPU 带来极大的开销。

  • 解决:

    • 控制自旋次数(如 JVM 的自适应自旋)。

    • 或者直接退化为锁(如synchronized的锁膨胀)。

    • 或者使用LongAdder分散热点数据(类似分段锁的思想)。

2. ABA 问题(最经典)
  • 问题:假设线程 1 执行 CAS 时,期望内存值为A。但在此时,线程 2 抢先将内存改成了B,然后又改回了A。此时线程 1 的 CAS 操作依然会成功,但线程 1 并不知道这中间发生过变化(比如链表节点的值被删了又加回来,可能导致引用错乱)。

  • 解决:引入版本号(Version)。每次修改不仅修改值,还要修改版本号(类似数据库的乐观锁)。Java 提供了AtomicStampedReference类,内部用int stamp记录版本;或者使用AtomicMarkableReference(用 boolean 记录是否被修改过)。

3. 只能保证一个共享变量的原子操作
  • 问题:一个 CAS 指令只能针对一个内存地址(一个变量)操作。

  • 解决:

    • 将多个变量封装成一个对象,使用AtomicReference对对象进行整体 CAS 替换。

    • 使用synchronized或Lock直接加锁。

💡 总结一句话

Java 的 CAS 是通过Unsafe类的本地方法,最终依靠 CPU 的lock cmpxchg指令实现硬件级别的原子性和可见性。它以极低的性能开销取代了传统的悲观锁,但必须防范其高并发下的 CPU 自旋开销以及常见的 ABA 问题。

返回列表