当前位置: 首页 > news >正文

并发编程——04 深入理解CASAtomic原子操作类详解

1 CAS

1.1 介绍

  • CAS(Compare And Swap,比较与交换),是非阻塞同步的实现原理,是 CPU 硬件层面支持的原子操作指令,从 CPU 层面保证“比较 + 交换”两个操作的原子性;

    • 该指令操作包含三个参数:内存值 V(实际要修改的变量,存在内存地址里)、预期值 E(你认为 V 应该是什么)、新值 N(想把 V 改成啥);
    • 执行规则:只有内存实际值 V 和预期值 E 相等时,才会把 V 更新成 N;如果不等,说明被其他线程改过,不更新,且返回当前内存实际值 V
    • 全程原子性:CPU 保证“比较 + 交换”不会被打断,要么全成功,要么全不执行,解决多线程并发冲突问题;

    在这里插入图片描述

  • CAS 是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。CAS 可以看做是乐观锁(对比数据库的悲观、乐观锁)的一种实现方式,Java 原子类中的递增操作就通过 CAS 自旋实现的。

1.2 使用

  • 在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作:

    // Object 类型字段的 CAS
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    // int 类型字段的 CAS
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    // long 类型字段的 CAS
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var5);
    
    • 它们都是 native 方法,由 Java 虚拟机提供具体实现,这意味着不同的 Java 虚拟机对它们的实现可能会略有不同;

    Unsafe 是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用;

    但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作直接内存空间的能力,但也破坏了 Java 的安全沙箱,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重。

  • compareAndSwapInt方法为例:该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作

    • Object var1对象实例(要操作的对象,比如下面代码中的 Entity 实例);

    • long var2字段的内存偏移量(对象里的字段在内存中的位置,需提前获取);

    • int var4预期值(你认为字段当前应该是什么值);

    • int var5新值(想把字段改成的值);

    • 返回值boolean,成功修改(预期值与实际值一致,且完成交换 )返回 true,否则 false

    为了方便获取 Unsafe 实例和字段偏移量,下面的示例代码中封装了 UnsafeFactory

    1. getUnsafe() 方法
      • 因为 Unsafe 的构造是受限的(JDK 限制直接 new),所以通过反射获取 Unsafe 类的 theUnsafe 静态字段,并用 setAccessible(true) 突破访问限制,拿到实例;
      • 这是 Java 里“非常规”但必要的手段,才能使用 Unsafe 的能力;
    2. getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) 方法
      • 用反射找到类的字段(clazz.getDeclaredField(fieldName)),再通过 unsafe.objectFieldOffset(...) 获取该字段在对象内存中的偏移量(后续 CAS 操作需要用这个偏移量定位字段);
    public class CASTest {public static void main(String[] args) {// 初始化Entity对象Entity entity = new Entity();// 用反射找到类的字段Unsafe unsafe = UnsafeFactory.getUnsafe();// 获取该字段在对象内存中的偏移量long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");boolean successful;// 执行三次 CAS 操作// 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段新值// 第一次:尝试把 x 从 0 改成 3(预期值 0,新值 3)。如果 x 当前是 0,修改成功,x 变为 3successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);System.out.println(successful + "\t" + entity.x);// 第二次:尝试把 x 从 3 改成 5(预期值 3,新值 5)。如果 x 当前是 3,修改成功,x 变为 5successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);System.out.println(successful + "\t" + entity.x);// 第三次:尝试把 x 从 3 改成 8(预期值 3,但此时 x 已经是 5 了),修改失败,x 保持 5successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);System.out.println(successful + "\t" + entity.x);}
    }public class UnsafeFactory {/*** 获取 Unsafe 对象* @return*/public static Unsafe getUnsafe() {try {// 用反射找到类的字段Field field = Unsafe.class.getDeclaredField("theUnsafe");field.setAccessible(true);return (Unsafe) field.get(null);} catch (Exception e) {e.printStackTrace();}return null;}/*** 获取字段的内存偏移量* @param unsafe* @param clazz* @param fieldName* @return*/public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {try {// 获取unsafe在对象内存中的偏移量return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));} catch (NoSuchFieldException e) {throw new Error(e);}}
    }
    

    在这里插入图片描述

1.3 应用场景

  • 多线程环境下,i++ 这类操作不是原子的(会拆成“读-改-写”三步),容易出现线程安全问题。AtomicInteger利用CAS + 自旋,无需加锁就能实现线程安全的整数操作,是 Java 并发包的基础工具类;

  • AtomicInteger 的关键代码:

    public class AtomicInteger extends Number implements java.io.Serializable {// 1. 核心字段:存储实际整数值,用 volatile 保证多线程可见性private volatile int value;// 2. 借助 Unsafe 类实现底层 CASprivate static final Unsafe unsafe = Unsafe.getUnsafe();// 3. valueOffset:value 字段在对象内存中的偏移量(定位字段用)private static final long valueOffset;// 4. 静态代码块:初始化 valueOffsetstatic {try {// 通过 Unsafe 获取 value 字段的内存偏移量valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}
    }
    
    • value 字段:用 volatile 修饰,确保多线程下修改可见性(一个线程改了,其他线程能立刻看到新值);

    • Unsafe + valueOffset

      • Unsafe 是操作底层的“后门”,valueOffsetvalue 字段在 AtomicInteger 对象内存中的偏移地址(类似 “对象起始地址 + 偏移量 = 字段实际地址”);
      • 通过 valueOffsetUnsafe 能精准定位到 value 字段的内存位置,执行 CAS 操作;
  • 结合示意图,理解 AtomicInteger 自增时的 CAS 流程

    • AtomicInteger 对象有个 基地址 baseAddress(比如 0x110000);
    • valueOffsetvalue 字段相对基地址的偏移量(比如 12 字节 ),所以 value 的实际内存地址 = baseAddress + valueOffset(即 0x11000c);

    在这里插入图片描述

    • 假设当前 value = 1,线程要执行自增(预期改成 2);
      • 步骤 1:通过 baseAddress + valueOffset 找到 value 的内存地址,读取当前值(预期值 E = 1);
      • 步骤 2:执行 CAS 操作:比较内存中的实际值和预期值 E
        • 如果相等(没被其他线程修改),就把 value 改成新值(2),返回成功;
        • 如果不等(被其他线程改过,比如变成 2 了),CAS 失败,自旋重试(重新读值、再比较交换),直到成功;
    • 总结:用Unsafe定位字段内存地址,靠 CAS 指令实现原子操作,最终让AtomicInteger无需加锁也能线程安全;
  • CAS 在AtomicInteger中的价值:

    • 无锁高效:不用synchronized加锁,避免线程阻塞,适合高并发、冲突少的场景;

    • 原子性保障:靠 CPU 硬件指令 + Unsafe调用,确保“读-改-写”是原子操作,解决多线程并发问题;

    • 自旋重试:失败时不断重试(直到成功),体现乐观锁思想(默认认为冲突少,重试成本低);

  • AtomicInteger是 Java 并发包的基础,类似的还有AtomicLongAtomicBoolean等。更复杂的并发工具(如AQSConcurrentHashMap)也依赖 CAS 实现无锁化的线程安全,比如:

    • AQS(AbstractQueuedSynchronizer):用 CAS 实现队列节点的原子性状态修改;

    • ConcurrentHashMap:在扩容、节点操作时,用 CAS 保证线程安全,替代分段锁的部分场景。

1.4 源码分析

  • 从 Java 代码到最终硬件执行,CAS 走了这样的流程:

    • Java 代码(如AtomicIntegercompareAndSet)>>
    • Unsafe类的native方法(compareAndSwapInt) >>
    • Hotspot 虚拟机 C++ 实现(Unsafe_CompareAndSwapInt) >>
    • CPU 指令(如 x86 的cmpxchg
  • Hotspot 虚拟机对Unsafe类中的compareAndSwapInt方法的实现如下:unsafe.cpp

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");// 把 Java 对象 obj 转成虚拟机内部的 oop(对象表示)oop p = JNIHandles::resolve(obj);// 根据对象和偏移量,计算 value 的内存地址 addrjint* addr = (jint *) index_oop_from_field_offset_long(p, offset);// 核心:调用 Atomic::cmpxchg,执行 CAS 逻辑// x:要交换的值,e:要比较的值,addr:value的内存地址// CAS成功,返回期望值e,等于e,此方法返回true// CAS失败,返回内存中的value值,不等于e,此方法返回falsereturn (jint)Atomic::cmpxchg(x, addr, e) == e;
    UNSAFE_END2
    
  • 核心逻辑在Atomic::cmpxchg方法中,该方法的实现在不同的操作系统和不同CPU中会不同。Linux_64x 的为例,看atomic_linux_x86.inline.hpp例的Atomic::cmpxchg的实现:

    inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {int mp = os::is_MP(); // 判断是否是多处理器环境// LOCK_IF_MP(%4):在多处理器环境下,给 cmpxchg 指令添加 lock 前缀,以达到内存屏障的效果// cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等// 如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给 exchange_value// 这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)": "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory");return exchange_value;
    }
    
    • LOCK_IF_MP(mp) 的作用:多处理器环境下,给 cmpxchglock 前缀,强制指令全局可见(其他 CPU 核心能立刻看到修改),避免缓存不一致问题,保障原子性;
    • cmpxchg 指令逻辑
      • 输入:exchange_value(要交换的新值)、dest(目标内存地址)、compare_value(预期值);
      • 执行:比较 dest 地址的值和 compare_value
        • 相等:把 exchange_value 写入 dest,并设置标志位(表示成功);
        • 不等:把 dest 当前值写入 exchange_value(返回旧值,表示失败);
  • cmpxchg的详细执行流程:汇编代码里的 __asm__ volatile内联汇编,直接操作 CPU 寄存器,流程如下

    1. 寄存器分配
      • 输出:"=a" (exchange_value) → 把结果存入 eax 寄存器;
      • 输入:"r" (exchange_value)(新值)、"a" (compare_value)(预期值)、"r" (dest)(目标地址)、"r" (mp)(是否多处理器标记);
    2. 执行 cmpxchgl %1,(%3)
      • %1exchange_value(新值,存在某个通用寄存器);
      • (%3)dest(目标内存地址,解引用后操作);
      • 指令逻辑:比较 eax(存 compare_value )和 dest 的值;
        • 相等 → 把 %1(新值)写入 desteax 设为新值;
        • 不等 → 把 dest 当前值读入 eaxeax 设为旧值;
    3. 返回结果exchange_value 最终是 eax 的值,用来判断 CAS 是否成功(和预期值相等则成功);
  • 现代处理器指令集架构基本上都会提供 CAS 指令,例如 x86 和 IA-64 架构中的cmpxchgl指令和comxchgq指令,sparc 架构中的cas指令和casx指令。不管是 Hotspot 中的Atomic::cmpxchg方法,还是 Java 中的compareAndSwapInt方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。总结一下 CAS 的原子性是如何保证的?

    • 硬件指令保障cmpxchg 是 CPU 提供的原子指令,执行时不会被中断,要么全成功、要么全失败;

    • 内存屏障辅助:多处理器下的 lock 前缀,保证指令执行时内存全局可见,避免其他线程/CPU 核心看到“中间状态”,进一步强化原子性;

  • CAS 底层实现的完整逻辑:

    1. Java 层调用 Unsafe.compareAndSwapInt,触发 JVM 的 native 方法;
    2. JVM 层通过 Unsafe_CompareAndSwapInt,找到字段的内存地址,调用 Atomic::cmpxchg
    3. 虚拟机层根据 CPU 架构(如 x86 ),调用对应的 cmpxchg 指令,利用硬件原子性完成 “比较 + 交换”;
    4. 最终靠 CPU 指令的原子性、内存屏障,保障 CAS 操作不被打断,实现线程安全。

1.5 缺陷

  • 自旋 CAS 长时间不成功,给 CPU 带来极大开销

    • 当多个线程竞争同一个变量时,若某线程执行 CAS 操作(比如 AtomicInteger 的自增),发现预期值和内存值不一致,就会自旋重试(不断重新读值、比较、交换);

    • 如果高并发下冲突频繁,线程会持续在用户态循环重试,占用 CPU 时间片却做无用功。极端情况下,大量线程自旋会让 CPU 资源被“空转”耗尽,拖慢整个系统;

  • 只能保证一个共享变量的原子操作

    • CAS 的设计逻辑是针对单个内存地址(通过偏移量定位字段)做“比较 + 交换”。如果业务需要同时保证多个变量的原子性操作(比如转账时,要同时扣减 A 的余额、增加 B 的余额),CAS 无法直接支持;
    • 这种场景下,要么用锁(如synchronized)包裹多个变量操作,要么手动组合多个 CAS 操作(但会复杂且容易出错),CAS 的“无锁优势”难以体现;
  • ABA 问题

    • 线程 1 想把变量从 A 改成 B,执行 CAS 前,变量被线程 2 改成 C,又被线程 3 改回 A。此时线程 1 执行 CAS 时,发现“预期值 A == 内存值 A”,就会认为变量没被修改过,成功执行交换。但实际上变量经历了A→C→A的变化,可能引发业务逻辑错误;
    • 某些场景下(如链表节点操作、金额变更),这种“值被改回原值,但中间过程有变化”的情况会被 CAS 忽略,导致逻辑漏洞

1.6 ABA问题及其解决方案

  • 什么是 ABA 问题?

    • CAS 依赖“比较值是否变化”来判断是否执行更新,但如果变量被修改后又改回原值,CAS 会认为“值没变化”,但中间可能经历了业务不允许的修改;

    • 例:

      在这里插入图片描述

    • 初始值:共享变量是 10

    • Thread2 操作:先把值改成 20,又改回 10

    • Thread1 操作:读取值是 10,执行 CAS 时发现“预期值 == 当前值”,认为变量没被修改过,成功执行更新;

    • 但实际变量经历了 10→20→10的变化,中间过程被 CAS 忽略,可能引发业务逻辑错误(比如链表节点被删除又插入、金额被篡改又改回);

  • 数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。同样,Java 也提供了相应的原子引用类AtomicStampedReference给变量增加一个“版本号(stamp)”,修改时不仅比较值,还要比较版本号。只要值被修改过(无论是否改回原值),版本号就会递增,CAS 会因为版本号不一致失败;

    public class AtomicStampedReferenceTest {public static void main(String[] args) {// 初始化:值是1,版本号是1AtomicStampedReference atomicStampedReference = new AtomicStampedReference(1, 1);// Thread1:读取值和版本号,阻塞后尝试更新new Thread(() -> {int[] stampHolder = new int[1]; int value = (int) atomicStampedReference.get(stampHolder); int stamp = stampHolder[0]; log.debug("Thread1 read value: " + value + ", stamp: " + stamp); // 读1,版本1LockSupport.parkNanos(1000000000L); // 阻塞1秒// CAS:需要值=1、版本=1,才改成3,同时版本+1if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {log.debug("Thread1 update from " + value + " to 3");} else {log.debug("Thread1 update fail!"); // 实际会走到这里}}, "Thread1").start();// Thread2:修改值(1→2→1),同时版本号递增new Thread(() -> {int[] stampHolder = new int[1]; int value = (int) atomicStampedReference.get(stampHolder); int stamp = stampHolder[0]; log.debug("Thread2 read value: " + value + ", stamp: " + stamp); // 读1,版本1// 1→2,版本从1→2if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {log.debug("Thread2 update from " + value + " to 2");// 模拟其他操作value = (int) atomicStampedReference.get(stampHolder); stamp = stampHolder[0]; log.debug("Thread2 read value: " + value + ", stamp: " + stamp); // 读2,版本2// 2→1,版本从2→3if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {log.debug("Thread2 update from " + value + " to 1");}}}, "Thread2").start();}
    }
    
    • Thread1 读值 1、版本 1,阻塞后恢复;
    • Thread2 已把值从 1→2→1,版本号从 1→2→3
    • Thread1 执行 compareAndSet(1, 3, 1, 2) 时,发现当前版本是 3(不等于预期 1 ),CAS 失败,输出 Thread1 update fail!
  • 补充:如果业务不需要精确的版本号,只需要知道“变量是否被修改过”,可以用 AtomicMarkableReference。它用 boolean 类型的 mark 代替版本号,简单标记“是否变化”,是 AtomicStampedReference 的简化版。

2 Atomic 原子操作类

2.1 介绍

  • 在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,也会因为“读-改-写”三步非原子性,导致并发安全问题(结果错误),最常用的方法是通过synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在 JUC 下的atomic包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型;

  • atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在 Java 中则是使用 CAS(乐观锁)操作具体实现;

  • Java 的 java.util.concurrent.atomic 包提供了一系列原子类,按功能分以下几类:

    • 基本类型原子类AtomicIntegerAtomicLongAtomicBoolean

      • 解决 intlongboolean 等基本类型的 线程安全更新(替代 synchronized 加锁);

      • 内部用 volatile 保证可见性,用 CAS + 自旋 实现原子操作(如 getAndIncrement 自增);

    • 引用类型原子类AtomicReferenceAtomicStampedReferenceAtomicMarkableReference

      • AtomicReference:解决对象引用的原子更新(类似 AtomicInteger,但针对对象);

      • AtomicStampedReference:带版本号的引用原子类,解决 CAS 的 ABA 问题

      • AtomicMarkableReference:带布尔标记的引用原子类,简单标记 “是否被修改过”,比版本号更轻量;

    • 数组类型原子类AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

      • 解决数组元素的原子更新(支持通过索引,安全修改数组里的 intlong、对象引用);
    • 对象属性原子修改器AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater

      • 无需修改类的源码,通过反射实现对对象某个字段的原子更新(要求字段是 volatile 修饰的);

      • 对第三方库的类字段做原子更新,或不想让类依赖原子类时使用;

    • 原子类型累加器(JDK1.8+):DoubleAccumulatorDoubleAdderLongAccumulatorLongAdderStriped64

      • 专门优化高并发下的累加操作(如统计 QPS、计数),比 AtomicLong 更高效;

      • 内部用分段锁思想(分散竞争),适合写多读少场景;

  • 所有原子类的底层,都是通过 CAS 操作 实现原子性:

    1. 读取当前值(带 volatile 保证可见性);
    2. 用 CAS 尝试更新:比较当前值和预期值,一致则更新,否则自旋重试(或失败);
    3. 靠 CPU 指令的原子性(如 cmpxchg),避免加锁,提升效率。

2.2 原子更新基本类型

  • AtomicInteger为例,核心方法都是基于Unsafe类的 CAS 操作实现,保证线程安全。常见方法如下:

    // 以原子的方式将实例中的原值加1,返回的是自增前的旧值
    public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
    }// 自旋 + CAS 保证将实例中的值更新为新值,并返回旧值
    // 如果 CAS 失败(值被其他线程修改),就重新获取值再尝试,直到成功
    public final boolean getAndSet(boolean newValue) {boolean prev;do {prev = get(); // 循环获取当前值} while (!compareAndSet(prev, newValue)); // CAS 重试,直到成功return prev;
    }// 以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果
    public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }// 以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果
    public final int addAndGet(int delta) {return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
    
  • Unsafe类的getAndAddIntAtomicInteger方法的底层支撑,代码逻辑:

    public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {// 获取当前内存中的值(用getIntVolatile保证多线程可见性)var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 尝试CAS操作:如果内存中的值还是var5,就更新为var5 + var4;否则重试(自旋)// 返回旧值 var5return var5;
    }
    
  • 这种 CAS 失败自旋的操作存在什么问题?

    • 如果多个线程频繁竞争同一个 AtomicInteger(比如高并发计数),CAS 失败会导致线程持续自旋(在 do-while 里循环重试),占用 CPU 时间片却无法推进业务逻辑
    • 影响:极端情况下,大量线程自旋会让 CPU 资源被 “空耗”,拖慢整个系统的性能。

2.3 原子更新数组类型

  • AtomicIntegerArray为例:

    // 以原子更新的方式将数组中索引为i的元素与输入值相加
    public final int addAndGet(int i, int delta) {return getAndAdd(i, delta) + delta;
    }// 以原子更新的方式将数组中索引为i的元素自增加1
    public final int getAndIncrement(int i) {return getAndAdd(i, 1);
    }// 将数组中索引为i的位置的元素进行更新
    public final boolean compareAndSet(int i, int expect, int update) {return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }
    

2.4 原子更新引用类型

  • AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性;

    public class AtomicReferenceTest {public static void main( String[] args ) {User user1 = new User("张三", 23);User user2 = new User("李四", 25);User user3 = new User("王五", 20);// AtomicReference<User> 表示:这个原子引用专门管理 User 类型的对象AtomicReference<User> atomicReference = new AtomicReference<>();// 让 atomicReference 初始指向 user1atomicReference.set(user1);// compareAndSet(预期引用, 新引用):// 逻辑:如果当前 atomicReference 指向的对象是预期引用(这里是 user1),就把它改成新引用(这里是 user2)// 效果:原子操作,多线程下不会冲突。如果有其他线程同时修改,只有一个能成功atomicReference.compareAndSet(user1, user2);System.out.println(atomicReference.get());// 尝试把引用从 user1 改成 user3(但此时已经是 user2 了,修改失败)atomicReference.compareAndSet(user1, user3);System.out.println(atomicReference.get()); // 输出 user2(不变)// 失败原因:当前 atomicReference 指向的是 user2,而预期引用是 user1,不匹配,所以 compareAndSet 失败,引用保持 user2}
    }@Data
    @AllArgsConstructor
    class User {private String name;private Integer age;
    }
    

2.5 对象属性原子修改器

  • AtomicIntegerFieldUpdater可以线程安全地更新对象中的整型变量

    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;public class AtomicIntegerFieldUpdaterTest {// 定义一个类,用于演示 AtomicIntegerFieldUpdater 的使用public static class Candidate {// (1)volatile 修饰的字段:满足 AtomicIntegerFieldUpdater 的约束,保证多线程可见性volatile int score = 0; // 对比用的 AtomicInteger 字段(非必须,仅用于对比演示)AtomicInteger score2 = new AtomicInteger(); }// (2)创建 AtomicIntegerFieldUpdater:用于原子更新 Candidate 类的 score 字段// 约束:操作的字段必须是 volatile 修饰、非 static、非 final,且可访问(这里 score 是 default 访问权限,当前类可访问)public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");// 真实分数的原子计数器(用于对比,模拟准确值)public static AtomicInteger realScore = new AtomicInteger();public static void main(String[] args) throws InterruptedException {// 创建 Candidate 实例,所有线程共享该实例的 score 字段final Candidate candidate = new Candidate();// 准备 10000 个线程(数组存储线程,方便后续 join)Thread[] t = new Thread[10000];for (int i = 0; i < 10000; i++) {t[i] = new Thread(new Runnable() {@Overridepublic void run() {// 随机概率执行更新操作if (Math.random() > 0.4) { // 方式 1:使用AtomicIntegerFieldUpdater原子地更新volatile int score字段candidate.score2.incrementAndGet(); // 方式 2:直接使用AtomicInteger的原子操作scoreUpdater.incrementAndGet(candidate); // 方式 3:同样使用AtomicInteger的原子操作realScore.incrementAndGet();// 以上三种方式都提供了完整的原子性:// AtomicIntegerFieldUpdater利用CAS操作保证原子性// AtomicInteger.incrementAndGet()内部也使用CAS操作}}});// 启动线程t[i].start();}// 等待所有线程执行完毕(否则主线程可能提前打印结果)for (int i = 0; i < 10000; i++) {t[i].join();}// 打印结果:对比三种方式的最终值System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score);System.out.println("AtomicInteger Score=" + candidate.score2.get());System.out.println("realScore=" + realScore.get());}
    }
    
  • 对于AtomicIntegerFieldUpdater的使用稍微有一些限制和约束,约束如下:

    • 字段必须是 volatile 类型

      volatile int score = 0; 
      
      • volatile 保证多线程间的可见性,AtomicIntegerFieldUpdater 依赖此特性实现原子更新。如果字段不加 volatile,多线程下可能出现“更新丢失”或“可见性问题”;
    • 字段的访问权限与调用类匹配

      // AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score")
      // score 是 Candidate 类的 default 访问权限字段,当前类(AtomicIntegerFieldUpdaterTest)与 Candidate 同包,可访问
      
      • 若字段是 private,则调用类必须是字段所在类本身;若为 protected,则调用类需是子类。父类的 private 字段无法被子类的 AtomicIntegerFieldUpdater 操作;
    • 不能是 static 字段

      volatile int score = 0; // 非 static,符合要求
      // 若写成 static volatile int score = 0; 则无法用 AtomicIntegerFieldUpdater 操作
      
      • AtomicIntegerFieldUpdater 设计用于更新实例字段static 字段属于类级别的,需用其他方式(如 AtomicInteger 直接定义 static 实例);
    • 不能是 final 字段

      volatile int score = 0; // 非 final,符合要求
      // 若写成 final volatile int score = 0; 则无法更新(final 语义是不可变)
      
      • AtomicIntegerFieldUpdater 的目的是“原子更新字段值”,final 字段不可修改,冲突;
    • AtomicIntegerFieldUpdater 只能操作 int 类型字段

      volatile int score = 0; // int 类型,符合要求
      // 若字段是 Integer 类型(包装类),则需用 AtomicReferenceFieldUpdater
      

2.6 原子类型累加器详解

  • 下面以LongAdder为例。

2.6.1 LongAdder解决高并发环境下基本类型原子类的自旋瓶颈问题

  • AtomicLongCAS + 自旋 实现原子操作(如 getAndAdd):

    public final long getAndAdd(long delta) {return unsafe.getAndAddLong(this, valueOffset, delta);
    }// 底层自旋逻辑
    public final long getAndAddLong(Object var1, long var2, long var4) {long var6;do {// 读取当前值(volatile 保证可见性)var6 = this.getLongVolatile(var1, var2); } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); // CAS 失败则自旋重试,直到成功return var6;
    }
    
    • 低并发场景下:冲突少,自旋次数少,效率高。但是在高并发场景:大量线程同时竞争,CAS 失败导致频繁自旋,CPU 空转严重,成为性能瓶颈;
    • 这就是LongAdder引入的初衷——解决高并发环境下AtomicIntegerAtomicLong的自旋瓶颈问题;
  • LongAdder 针对高并发优化,核心思路是**“分段累加”**:

    • 分散竞争:内部用多个 Cell(小计数器),线程竞争时优先更新自己的 Cell,避免全局竞争;

    • 最终合并:获取结果时,汇总所有 Cell 的值 + 基值(base),得到最终结果;

    • 这样,高并发下线程冲突被分散到不同 Cell,减少自旋次数,提升效率;

  • 下面用代码对比 AtomicLongLongAdder 在不同并发场景下的性能,验证 LongAdder 的优势:

    public class LongAdderTest {public static void main(String[] args) {testAtomicLongVSLongAdder(10, 10000);System.out.println("==================");testAtomicLongVSLongAdder(10, 200000);System.out.println("==================");testAtomicLongVSLongAdder(100, 200000);}// 控制变量(线程数、操作次数),对比两者耗时static void testAtomicLongVSLongAdder(final int threadCount, final int times) {try {long start = System.currentTimeMillis();testLongAdder(threadCount, times);long end = System.currentTimeMillis() - start;System.out.println("条件>>>>>>线程数:" + threadCount + ", 单线程操作计数" + times);System.out.println("结果>>>>>>LongAdder方式增加计数" + (threadCount * times) + "次,共计耗时:" + end);long start2 = System.currentTimeMillis();testAtomicLong(threadCount, times);long end2 = System.currentTimeMillis() - start2;System.out.println("条件>>>>>>线程数:" + threadCount + ", 单线程操作计数" + times);System.out.println("结果>>>>>>AtomicLong方式增加计数" + (threadCount * times) + "次,共计耗时:" + end2);} catch (InterruptedException e) {e.printStackTrace();}}// 用AtomicLong做累加,模拟高并发下的自旋竞争static void testAtomicLong(final int threadCount, final int times) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(threadCount);AtomicLong atomicLong = new AtomicLong();for (int i = 0; i < threadCount; i++) {new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < times; j++) {atomicLong.incrementAndGet();}countDownLatch.countDown();}}, "my-thread" + i).start();}countDownLatch.await();}// 用LongAdder做累加,验证分段累加的效率static void testLongAdder(final int threadCount, final int times) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(threadCount);LongAdder longAdder = new LongAdder();for (int i = 0; i < threadCount; i++) {new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < times; j++) {longAdder.add(1); // 线程局部竞争分散到 Cell,减少冲突}countDownLatch.countDown();}}, "my-thread" + i).start();}countDownLatch.await(); // 等待所有线程完成}
    }
    

    在这里插入图片描述

    场景(线程数/单线程操作数)LongAdder 耗时AtomicLong 耗时结论
    10 线程 × 10000 次11ms8ms低并发下,AtomicLong 更轻量(无分段开销)
    10 线程 × 200000 次18ms57ms高并发时,LongAdder 开始反超(分段减少自旋)
    100 线程 × 200000 次49ms461ms极高并发下,LongAdder 优势极大(自旋瓶颈被解决)
  • 适用场景总结

    • AtomicLong:适合低并发、读多写少场景,轻量无额外开销;

    • LongAdder:适合高并发、写多读少场景(如统计 QPS、接口调用次数),通过分段累加规避自旋瓶颈。

2.6.2 LongAdder原理

  • AtomicLong内部靠一个**全局变量 value**保存数值,所有线程的 CAS 操作都针对这个变量:

    • 低并发时,冲突少,自旋重试少,效率还行;
    • 高并发时,所有线程竞争同一个 value(热点),大量 CAS 失败导致自旋重试,CPU 空转严重,性能暴跌;
  • LongAdder分段累加的思路解决热点问题,核心逻辑:

    在这里插入图片描述

    • 拆分热点:base + Cell[] 数组

      • base:基础值,低并发时直接用 base 累加(类似 AtomicLong);
      • Cell[]:一个数组,每个 Cell 是一个小计数器。高并发时,线程会**“命中”不同的 Cell**,对自己的 Cell 做 CAS 操作,分散竞争;
    • 执行流程:

      • 初始尝试:线程先尝试对 base 做 CAS 累加;
      • 分散到 Cell:如果 base 竞争激烈(CAS 失败),线程会找到一个空闲的 Cell(数组中的槽),对该 Cell 的值做 CAS 累加;
      • 结果合并:需要获取最终值时,汇总 base + 所有 Cell 的值,得到总数;
    • 核心优势:冲突概率暴跌

      • AtomicLong所有线程竞争 1 个变量,冲突概率高;
      • LongAdder线程分散到多个 Cell,每个线程只竞争自己的 Cell,冲突概率大幅降低,自旋重试减少,高并发下性能更好。

2.6.3 LongAdder的内部结构

/** Number of CPUS, to place bound on table size */
// CPU核数,决定槽数组的大小
// 初始化 cells 数组时,大小通常与 CPU 核数相关(默认不超过核数,保证分散均匀) 
static final int NCPU = Runtime.getRuntime().availableProcessors();/*** Table of cells. When non-null, size is a power of 2.*/
// 高并发时,线程会分散到不同的 Cell(数组的槽)做累加,避免全局竞争
// 数组大小是 2 的幂次(方便哈希取模,均匀分散线程)
// 用 volatile 修饰,保证多线程可见性
transient volatile Cell[] cells;/*** Base value, used mainly when there is no contention, but also as* a fallback during table initialization races. Updated via CAS.*//***  基础累加值(低并发首选):*  1. 没有遇到并发竞争时,直接使用base累加数值*  2. 初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cells初始化),其他竞争失败的线程会将数值累加到base上*/
transient volatile long base;/*** Spinlock (locked via CAS) used when resizing and/or creating Cells.*/
// 自旋锁(控制 cells 数组的初始化/扩容)
// 作为自旋锁,保证 cells 数组的初始化和扩容是原子操作(避免多线程同时修改数组) 
// 实现:通过 CAS 操作cellsBusy(0 表示空闲,1 表示占用),实现轻量级锁
transient volatile int cellsBusy;
  • LongAdder 内部定义了 Cell 类,每个 Cell 是一个独立的小计数器:

    @sun.misc.Contended static final class Cell {volatile long value; // 每个 Cell 保存的累加值Cell(long x) { value = x; }// 核心:CAS 操作当前 Cell 的 valuefinal boolean cas(long cmp, long val) {return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);}// Unsafe 相关配置(用于 CAS 操作)private static final sun.misc.Unsafe UNSAFE;private static final long valueOffset;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> ak = Cell.class;valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));} catch (Exception e) {throw new Error(e);}}
    }
    
    • @sun.misc.Contended:(JDK 8+)解决伪共享问题。如果多个 Cell 被 CPU 缓存行加载,可能导致无关线程修改互相影响。加这个注解后,JVM 会为每个 Cell 分配独立缓存行,减少竞争;

    • volatile long value:保证 Cell 内部值的多线程可见性;

    • cas 方法:通过 Unsafe 类实现 CAS 操作,保证单个 Cell 累加的原子性;

  • 整体逻辑:

    • 低并发(无竞争):线程直接 CAS 操作 base,高效且无额外开销;
    • 高并发(有竞争)
      • 线程尝试初始化 cells 数组(通过 cellsBusy 自旋锁保证原子性);
      • 线程通过哈希算法(结合线程 ID 等)找到对应的 Cell,对该 Cellvalue 做 CAS 累加;
      • 如果 Cell 数组需要扩容(如冲突仍多),同样通过 cellsBusy 锁控制,扩容为更大的 2 的幂次数组;
    • 获取结果:调用 sum() 时,汇总 base + 所有 Cellvalue,得到最终累加值。

2.6.4 LongAdder#add方法

  • LongAdder#add方法的逻辑如下图:

    在这里插入图片描述

  • 设计精妙之处:延迟 CAS + 分散冲突

    • 尽量用 base:低并发时直接更新 base,避免 cells 数组的额外开销;
    • 冲突后用 cells:一旦出现并发冲突,后续操作转移到 cells 数组,分散热点;
    • 延迟 CAS 重试:通过 longAccumulate 集中处理冲突,而不是让线程在单个 Cell 上自旋重试,减少 CPU 空转。

2.6.5 Striped64#longAccumulate方法

在这里插入图片描述

2.6.6 LongAdder#sum方法

  • 返回 LongAdder 当前的累加总和,即**“当前时刻”**所有 baseCell 数组的值的总和;

    public long sum() {Cell[] as = cells; Cell a;long sum = base; // 1. 先累加 base 的值if (as != null) { // 2. 如果 cells 数组已初始化,遍历累加每个 Cell 的值for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum; // 3. 返回总和
    }
    
  • 高并发场景下,sum 结果可能不是 “调用瞬间的原子快照”,原因有二:

    • 无锁遍历 cells,无法阻止并发修改:计算 sum 时,没有对 cells 数组加锁。如果其他线程正在修改 Cell 的值(如 add 操作),或扩容 cells 数组,sum 遍历到的 Cell 值可能是**“部分更新后”**的状态;
    • 遍历和修改存在时间差:即使 cells 数组遍历完成,返回 sum 时,其他线程可能又修改了 baseCell 的值。因此,sum 的结果是**“调用期间多个时刻的值的混合”**,不是绝对精确的原子值。
http://www.dtcms.com/a/355373.html

相关文章:

  • Qt 中日志级别
  • JS中的String总结
  • Linux 环境源码安装 Docker
  • 影石insta360 DevOps工程师一面记录
  • 学习嵌入式之驱动——I2C子系统
  • 搭建一个Spring cloud 非阻塞式微服务架构
  • 任天堂NDS中文游戏ROM精选毕业整合包整理分享! +NDS模拟器
  • 使用Docker搭建StackEdit在线MarkDown编辑器
  • 如何通过docker进行本地部署?
  • 企业内部机密视频安全保护|如何防止企业内部机密视频泄露?
  • (附源码)基于Spring Boot公务员考试信息管理系统设计与实现
  • GitLab 配置 Pipeline 的过程
  • linux 网络:协议及Wireshark抓包工具的使用
  • Elasticsearch冷热架构:性能与成本完美平衡
  • 《深入浅出 Node.js》分享精简大纲
  • linu 网络 :TCP粘包及UDP
  • 软件设计师备考-(五)计算机网络
  • 客户端是否都需要主动发送`FindService`报文来寻找服务
  • FPGA开发技能(12)matlab图片处理助力FPGA开发
  • 【温室气体数据集】GlobalView 数据概述
  • Kotlin 协程之Channel 的高阶应用
  • RAGFlow
  • plantsimulation知识点 一条轨道上多台RGV如何引用
  • 【Big Data】Presto db 一种语言、一个接口搭建可靠、高效数据湖仓
  • NineData 最新发布 SQL Server 双向实时同步功能
  • 手机上访问你电脑上的前端项目
  • Rust 登堂 之 类型转换(三)
  • 趣味学Rust基础篇(数据类型)
  • Python Discord Logo
  • 【SpringAI】快速上手,详解项目快速集成主流大模型DeepSeek,ChatGPT