Loading

了解CAS与使用

了解CAS与使用

一、CAS 介绍

1.1 什么是 CAS

  • 定义:CAS(Compare And Swap,比较与交换)是 CPU 硬件层面的原子指令,是无锁同步的实现原理,可看作乐观锁的一种实现方式。

  • 核心逻辑:操作包含三个参数 ——内存值 V(目标变量的内存地址值)、预期值 E(线程认为变量应有的值)、新值 N(线程要更新的新值);执行时,当且仅当 V == E 时,才将 V 更新为 N,无论是否更新,都会返回旧的内存值 V,整个过程是原子操作(硬件层面保障)。

  • Java 伪代码模拟

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldRamAddress = accessMemory(ramAddress); // 读取内存值V
        if (oldRamAddress == expectedValue) { // 比较V与E
            ramAddress = newValue; // 相等则更新为N
        }
        return oldRamAddress; // 返回旧值V
    }
    

1.2 CAS 的 Java 支持(Unsafe 类)

  • Unsafe 类角色:位于 sun.misc 包,提供低级别、不安全操作(如直接访问内存),是 CAS 操作的底层支持,其 CAS 方法为 native 方法(由 JVM 实现,不同虚拟机可能有差异)。

  • 核心 CAS 方法

    方法声明 作用
    compareAndSwapObject(Object var1, long var2, Object var4, Object var5) 原子更新对象类型变量
    compareAndSwapInt(Object var1, long var2, int var4, int var5) 原子更新 int 类型变量
    compareAndSwapLong(Object var1, long var2, long var4, long var6) 原子更新 long 类型变量
  • 参数说明(以 compareAndSwapInt 为例):

    • var1:对象实例(要更新的变量所属对象)
    • var2:内存偏移量(变量在对象中的内存地址偏移,通过 unsafe.objectFieldOffset() 获取)
    • var4:字段期望值 E
    • var5:字段新值 N

1.3 CAS 的应用场景

CAS 在 Java 并发组件中应用广泛,核心场景包括:

  • java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)
  • Java AQS(AbstractQueuedSynchronizer,抽象队列同步器)
  • CurrentHashMap(并发哈希表)

1.4 CAS 的源码分析(Hotspot 虚拟机)

1.4.1 核心流程

  1. Unsafe_CompareAndSwapInt 方法:接收对象、偏移量、期望值、新值,解析对象地址,计算目标变量的内存地址,调用 Atomic::cmpxchg 执行 CAS 逻辑。

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
        UnsafeWrapper("Unsafe_CompareAndSwapInt");
        oop p = JNIHandles::resolve(obj);
        jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算变量内存地址
        return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 执行CAS并判断结果
    UNSAFE_END
    
  2. Atomic::cmpxchg 方法(以 Linux x86 为例):依赖 CPU 指令 cmpxchgl,多处理器环境下添加 lock 前缀保证内存屏障,实现 “比较 - 交换” 原子性。

    • 指令逻辑:比较 dest(内存地址)的值与 compare_value(E),相等则将 exchange_value(N)写入 dest,否则将 dest 值写入 exchange_value

1.5 CAS 的缺陷

缺陷类型 具体说明
自旋开销大 若 CAS 长时间失败(如高并发下),线程会持续自旋重试,占用大量 CPU 资源
仅支持单个共享变量 CAS 只能保证单个变量更新的原子性,无法直接实现多个变量的原子操作(需组合成对象间接实现)
ABA 问题 线程 1 读取值为 A,线程 2 将 A→B 再→A,线程 1 再次 CAS 时,认为值未修改而更新成功,忽略中间变更

1.6 ABA 问题及解决方案

1.6.1 什么是 ABA 问题

  • 场景:多线程操作原子类时,某线程短时间内将值从 A 改为 B,再改回 A,其他线程无法感知中间变更,导致 CAS 误判成功。
  • 代码示例:AtomicInteger 初始值为 1,Thread2 将 1→2→1,Thread1 阻塞后 CAS 1→3 仍成功,误以为值未修改。

1.6.2 解决方案

  1. AtomicStampedReference
    • 原理:维护 “引用值(reference)+ 版本号(stamp)”,每次修改时不仅更新值,还将版本号 +1,CAS 时需同时比较 “值” 和 “版本号”,确保中间无变更。
    • 核心方法:compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)
  2. AtomicMarkableReference
    • 原理:简化版 AtomicStampedReference,用 boolean 类型的 mark 标记值是否被修改(不关心修改次数,仅关心 “是否修改过”)。

二、Atomic 原子操作类介绍

2.1 原子类概述

  • 核心作用:在并发编程中,以乐观锁(CAS)实现线程安全的变量更新,替代悲观锁(synchronized),提升性能。
  • 分类:共 5 大类,覆盖基本类型、引用类型、数组、对象属性、累加场景,具体如下表:
分类 具体类名 核心用途
基本类型 AtomicInteger、AtomicLong、AtomicBoolean 原子更新 int、long、boolean 类型变量
引用类型 AtomicReference、AtomicStampedReference、AtomicMarkableReference 原子更新对象引用,解决 ABA 问题
数组类型 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 按索引原子更新数组中的元素
对象属性修改器 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater 原子更新对象的实例字段(无需修改字段所属类)
原子累加器(JDK1.8+) LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator 高并发场景下高效累加,性能优于传统原子类

2.2 各类原子类详解

2.2.1 原子更新基本类型(以 AtomicInteger 为例)

  • 核心方法

    方法声明 描述 返回值
    getAndIncrement() 原子自增 1,先返回旧值 自增前的旧值
    incrementAndGet() 原子自增 1,先自增再返回新值 自增后的新值
    getAndSet(int newValue) 原子更新为新值,返回旧值 更新前的旧值
    addAndGet(int delta) 原子累加 delta,返回新值 累加后的新值
  • 实现原理:依赖 Unsafe 的 getAndAddInt 方法,通过自旋 CAS 重试直到更新成功。

  • 测试示例:10 个线程各自增 10000 次,最终 sum 为 100000(线程安全)。

2.2.2 原子更新数组类型(以 AtomicIntegerArray 为例)

  • 核心方法

    方法声明 描述
    addAndGet(int i, int delta) 原子更新数组索引 i 的元素,累加 delta 后返回新值
    getAndIncrement(int i) 原子更新数组索引 i 的元素,自增 1 后返回旧值
    compareAndSet(int i, int expect, int update) 原子更新数组索引 i 的元素,预期值为 expect 则更新为 update
  • 特点:操作直接作用于数组元素,需传入数组索引,保证数组元素更新的线程安全。

2.2.3 原子更新引用类型(以 AtomicReference 为例)

  • 核心作用:封装普通对象引用,保证对象引用更新的线程安全(区别于基本类型,可操作自定义对象)。

  • 代码示例

    AtomicReference<User> atomicRef = new AtomicReference<>(user1);
    atomicRef.compareAndSet(user1, user2); // 原子将引用从user1改为user2
    
  • 扩展:AtomicStampedReference 和 AtomicMarkableReference 用于解决引用更新的 ABA 问题。

2.2.4 原子更新对象属性(以 AtomicIntegerFieldUpdater 为例)

  • 核心作用:无需修改对象类的源码,通过反射原子更新对象的整型实例字段。
  • 使用约束(共 5 点):
    1. 字段必须是 volatile 类型(保证线程间可见性);
    2. 字段访问权限与调用者匹配(如调用者可访问私有字段才能原子更新);
    3. 字段必须是 实例变量(不能加 static 关键字);
    4. 字段不能是 final 类型(final 不可修改,与原子更新冲突);
    5. 仅支持 int/long 基本类型(包装类型需用 AtomicReferenceFieldUpdater)。

2.2.5 原子累加器(以 LongAdder 为例)

  • 设计背景:高并发下,AtomicLong 的自旋 CAS 会因线程冲突频繁导致性能瓶颈,LongAdder 通过 “分散热点” 提升性能。

  • 核心原理

    • 结构:继承 Striped64,包含 base(非竞态时的基数)和 Cell[] 数组(竞态时,线程将值累加到各自的 Cell 槽中);
    • 逻辑:无并发冲突时,直接累加 base;有冲突时,线程通过哈希映射到 Cell[] 的某一槽,仅对该槽的 value 执行 CAS 操作,分散热点。
  • 核心方法

    方法声明 描述 特点
    add(long x) 原子累加 x 高并发时分散到 Cell 槽,冲突少
    sum() 计算总累加值(base + 所有 Cell 的 value) 非原子快照,高并发下返回近似值
  • 性能优势:线程数越多、并发操作次数越大,LongAdder 优势越明显(对比 AtomicLong),适用于 写多 read 少 的场景;低并发场景下,AtomicLong 足够高效。


关键问题

问题 1:CAS 能保证原子性的根本原因是什么?它与 synchronized 实现原子性的思路有何本质区别?

答案:

  • CAS 原子性的根本原因:CAS 是 CPU 硬件层面的原子指令(如 x86 架构的 cmpxchgl 指令),硬件直接保障 “比较 - 交换” 两个操作的不可分割性,无需软件层面的锁机制;即使在多处理器环境下,也可通过 lock 前缀指令实现内存屏障,防止指令重排序和多核心缓存不一致,进一步保证原子性。
  • 与 synchronized 的本质区别
    1. 锁策略:synchronized 是 悲观锁,默认认为线程会冲突,通过阻塞线程(获取锁→执行→释放锁)保证原子性,本质是 “以时间换空间”;CAS 是 乐观锁,默认认为线程冲突概率低,通过自旋重试(不阻塞线程)实现无锁同步,本质是 “以空间换时间”;
    2. 线程状态:synchronized 会导致线程阻塞 / 唤醒(内核态切换),开销大;CAS 线程始终处于运行态(用户态),仅自旋重试时占用 CPU,无内核态切换开销;
    3. 适用场景:synchronized 适用于冲突频繁、执行时间长的场景;CAS 适用于冲突少、执行时间短的场景(如原子类更新)。

问题 2:LongAdder 为何能在高并发场景下比 AtomicLong 性能更优?它的设计思路和局限性是什么?

答案:

  • 高并发性能优的核心原因:AtomicLong 所有线程竞争同一个热点变量 value,高并发下 CAS 冲突频繁,大量线程自旋重试导致 CPU 开销大;而 LongAdder 通过 “分散热点” 设计,将单个热点变量拆分为 “base 基数 + Cell [] 数组”,具体逻辑如下:
    1. 无冲突时:线程直接累加 base,与 AtomicLong 性能相当;
    2. 有冲突时:线程通过哈希算法映射到 Cell[] 数组的某一槽(Cell),仅对该槽的 value 执行 CAS 操作,不同线程操作不同 Cell,大幅减少冲突概率,提升并发效率。
  • 设计思路:基于 Striped64 抽象类,核心是 “将竞争分散到多个存储单元,降低单个单元的竞争强度”,本质是 “空间换时间” 的优化。
  • 局限性
    1. 求和非原子sum() 方法计算总计时,需遍历 Cell[] 数组累加 base 和所有 Cell 的 value,此过程无锁,若其他线程同时修改 Cell,sum() 返回的是 “近似值”,非调用时刻的原子快照;
    2. 功能单一:仅支持累加 / 递减操作,无法实现 AtomicLong 的 getAndSetcompareAndSet 等复杂原子操作;
    3. 内存开销大Cell[] 数组需占用额外内存(数组大小为 CPU 核数的 2 次幂),低并发场景下内存开销高于 AtomicLong。

问题 3:ABA 问题会导致什么业务风险?除了文档中的 AtomicStampedReference,还有其他解决方案吗?

答案:

  • ABA 问题的业务风险:若业务场景需感知变量的 “中间变更”,ABA 问题会导致逻辑错误,例如:
    1. 资金转账场景:用户 A 账户余额 100 元(A),线程 1 读取余额准备扣减 50 元,线程 2 先将 100→0(B,转账给 B)再→100(A,B 退款),线程 1 CAS 100→50 成功,但实际账户曾被清空,可能违反 “余额不足不允许扣减” 的业务规则;
    2. 链表节点操作场景:线程 1 准备删除链表节点 A,线程 2 先删除 A 再插入新节点 A,线程 1 CAS 时误判节点未变,导致链表结构损坏。
  • 其他解决方案
    1. 版本号机制(自定义实现):在变量中额外维护一个版本号字段(如 volatile long version),每次修改变量时版本号 +1,CAS 时同时比较 “变量值” 和 “版本号”,确保版本号连续递增(类似 AtomicStampedReference 的底层逻辑);
    2. 时间戳机制:用时间戳替代版本号,每次修改时记录当前时间戳,CAS 时比较 “值 + 时间戳”,若时间戳变化则说明中间有修改;
    3. 不可变对象:将变量设计为不可变对象(如 String),修改时创建新对象而非修改原对象,此时变量引用的变更不会出现 “旧值复用”,从根本上避免 ABA 问题(但会增加对象创建开销,适用于修改频率低的场景)。
posted @ 2025-10-03 18:40  流火无心  阅读(8)  评论(0)    收藏  举报