并发编程——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
:getUnsafe()
方法:- 因为
Unsafe
的构造是受限的(JDK 限制直接 new),所以通过反射获取Unsafe
类的theUnsafe
静态字段,并用setAccessible(true)
突破访问限制,拿到实例; - 这是 Java 里“非常规”但必要的手段,才能使用
Unsafe
的能力;
- 因为
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
是操作底层的“后门”,valueOffset
是value
字段在AtomicInteger
对象内存中的偏移地址(类似 “对象起始地址 + 偏移量 = 字段实际地址”);- 通过
valueOffset
,Unsafe
能精准定位到value
字段的内存位置,执行 CAS 操作;
-
-
结合示意图,理解
AtomicInteger
自增时的 CAS 流程AtomicInteger
对象有个 基地址baseAddress
(比如0x110000
);valueOffset
是value
字段相对基地址的偏移量(比如12
字节 ),所以value
的实际内存地址 =baseAddress + valueOffset
(即0x11000c
);
- 假设当前
value = 1
,线程要执行自增(预期改成2
);- 步骤 1:通过
baseAddress + valueOffset
找到value
的内存地址,读取当前值(预期值E = 1
); - 步骤 2:执行 CAS 操作:比较内存中的实际值和预期值
E
;- 如果相等(没被其他线程修改),就把
value
改成新值(2
),返回成功; - 如果不等(被其他线程改过,比如变成
2
了),CAS 失败,自旋重试(重新读值、再比较交换),直到成功;
- 如果相等(没被其他线程修改),就把
- 步骤 1:通过
- 总结:用
Unsafe
定位字段内存地址,靠 CAS 指令实现原子操作,最终让AtomicInteger
无需加锁也能线程安全;
-
CAS 在
AtomicInteger
中的价值:-
无锁高效:不用
synchronized
加锁,避免线程阻塞,适合高并发、冲突少的场景; -
原子性保障:靠 CPU 硬件指令 +
Unsafe
调用,确保“读-改-写”是原子操作,解决多线程并发问题; -
自旋重试:失败时不断重试(直到成功),体现乐观锁思想(默认认为冲突少,重试成本低);
-
-
AtomicInteger
是 Java 并发包的基础,类似的还有AtomicLong
、AtomicBoolean
等。更复杂的并发工具(如AQS
、ConcurrentHashMap
)也依赖 CAS 实现无锁化的线程安全,比如:-
AQS
(AbstractQueuedSynchronizer):用 CAS 实现队列节点的原子性状态修改; -
ConcurrentHashMap
:在扩容、节点操作时,用 CAS 保证线程安全,替代分段锁的部分场景。
-
1.4 源码分析
-
从 Java 代码到最终硬件执行,CAS 走了这样的流程:
- Java 代码(如
AtomicInteger
的compareAndSet
)>> Unsafe
类的native
方法(compareAndSwapInt
) >>- Hotspot 虚拟机 C++ 实现(
Unsafe_CompareAndSwapInt
) >> - CPU 指令(如 x86 的
cmpxchg
)
- Java 代码(如
-
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)
的作用:多处理器环境下,给cmpxchg
加lock
前缀,强制指令全局可见(其他 CPU 核心能立刻看到修改),避免缓存不一致问题,保障原子性;cmpxchg
指令逻辑:- 输入:
exchange_value
(要交换的新值)、dest
(目标内存地址)、compare_value
(预期值); - 执行:比较
dest
地址的值和compare_value
;- 相等:把
exchange_value
写入dest
,并设置标志位(表示成功); - 不等:把
dest
当前值写入exchange_value
(返回旧值,表示失败);
- 相等:把
- 输入:
-
cmpxchg
的详细执行流程:汇编代码里的__asm__ volatile
是 内联汇编,直接操作 CPU 寄存器,流程如下- 寄存器分配:
- 输出:
"=a" (exchange_value)
→ 把结果存入eax
寄存器; - 输入:
"r" (exchange_value)
(新值)、"a" (compare_value)
(预期值)、"r" (dest)
(目标地址)、"r" (mp)
(是否多处理器标记);
- 输出:
- 执行
cmpxchgl %1,(%3)
:%1
是exchange_value
(新值,存在某个通用寄存器);(%3)
是dest
(目标内存地址,解引用后操作);- 指令逻辑:比较
eax
(存compare_value
)和dest
的值;- 相等 → 把
%1
(新值)写入dest
,eax
设为新值; - 不等 → 把
dest
当前值读入eax
,eax
设为旧值;
- 相等 → 把
- 返回结果:
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 底层实现的完整逻辑:
- Java 层调用
Unsafe.compareAndSwapInt
,触发 JVM 的 native 方法; - JVM 层通过
Unsafe_CompareAndSwapInt
,找到字段的内存地址,调用Atomic::cmpxchg
; - 虚拟机层根据 CPU 架构(如 x86 ),调用对应的
cmpxchg
指令,利用硬件原子性完成 “比较 + 交换”; - 最终靠 CPU 指令的原子性、内存屏障,保障 CAS 操作不被打断,实现线程安全。
- Java 层调用
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 想把变量从
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!
;
- Thread1 读值
-
补充:如果业务不需要精确的版本号,只需要知道“变量是否被修改过”,可以用
AtomicMarkableReference
。它用boolean
类型的mark
代替版本号,简单标记“是否变化”,是AtomicStampedReference
的简化版。
2 Atomic 原子操作类
2.1 介绍
-
在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量
i=1
,比如多个线程执行i++
操作,也会因为“读-改-写”三步非原子性,导致并发安全问题(结果错误),最常用的方法是通过synchronized
进行控制来达到线程安全的目的。但是由于synchronized
是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在 JUC 下的atomic
包提供了一系列的操作简单、性能高效,并能保证线程安全的类去更新基本类型变量、数组元素、引用类型以及更新对象中的字段类型; -
atomic
包下的这些类都是采用的是乐观锁策略去原子更新数据,在 Java 中则是使用 CAS(乐观锁)操作具体实现; -
Java 的
java.util.concurrent.atomic
包提供了一系列原子类,按功能分以下几类:-
基本类型原子类:
AtomicInteger
、AtomicLong
、AtomicBoolean
-
解决
int
、long
、boolean
等基本类型的 线程安全更新(替代synchronized
加锁); -
内部用
volatile
保证可见性,用 CAS + 自旋 实现原子操作(如getAndIncrement
自增);
-
-
引用类型原子类:
AtomicReference
、AtomicStampedReference
、AtomicMarkableReference
-
AtomicReference
:解决对象引用的原子更新(类似AtomicInteger
,但针对对象); -
AtomicStampedReference
:带版本号的引用原子类,解决 CAS 的 ABA 问题; -
AtomicMarkableReference
:带布尔标记的引用原子类,简单标记 “是否被修改过”,比版本号更轻量;
-
-
数组类型原子类:
AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
- 解决数组元素的原子更新(支持通过索引,安全修改数组里的
int
、long
、对象引用);
- 解决数组元素的原子更新(支持通过索引,安全修改数组里的
-
对象属性原子修改器:
AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
、AtomicReferenceFieldUpdater
-
无需修改类的源码,通过反射实现对对象某个字段的原子更新(要求字段是
volatile
修饰的); -
对第三方库的类字段做原子更新,或不想让类依赖原子类时使用;
-
-
原子类型累加器(JDK1.8+):
DoubleAccumulator
、DoubleAdder
、LongAccumulator
、LongAdder
、Striped64
-
专门优化高并发下的累加操作(如统计 QPS、计数),比
AtomicLong
更高效; -
内部用分段锁思想(分散竞争),适合写多读少场景;
-
-
-
所有原子类的底层,都是通过 CAS 操作 实现原子性:
- 读取当前值(带
volatile
保证可见性); - 用 CAS 尝试更新:比较当前值和预期值,一致则更新,否则自旋重试(或失败);
- 靠 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
类的getAndAddInt
是AtomicInteger
方法的底层支撑,代码逻辑: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
解决高并发环境下基本类型原子类的自旋瓶颈问题
-
AtomicLong
靠 CAS + 自旋 实现原子操作(如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引入的初衷——解决高并发环境下
AtomicInteger
、AtomicLong
的自旋瓶颈问题;
-
LongAdder
针对高并发优化,核心思路是**“分段累加”**:-
分散竞争:内部用多个
Cell
(小计数器),线程竞争时优先更新自己的Cell
,避免全局竞争; -
最终合并:获取结果时,汇总所有
Cell
的值 + 基值(base
),得到最终结果; -
这样,高并发下线程冲突被分散到不同
Cell
,减少自旋次数,提升效率;
-
-
下面用代码对比
AtomicLong
和LongAdder
在不同并发场景下的性能,验证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 次 11ms 8ms 低并发下, AtomicLong
更轻量(无分段开销)10 线程 × 200000 次 18ms 57ms 高并发时, LongAdder
开始反超(分段减少自旋)100 线程 × 200000 次 49ms 461ms 极高并发下, 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
,对该Cell
的value
做 CAS 累加; - 如果
Cell
数组需要扩容(如冲突仍多),同样通过cellsBusy
锁控制,扩容为更大的 2 的幂次数组;
- 线程尝试初始化
- 获取结果:调用
sum()
时,汇总base
+ 所有Cell
的value
,得到最终累加值。
- 低并发(无竞争):线程直接 CAS 操作
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
当前的累加总和,即**“当前时刻”**所有base
和Cell
数组的值的总和;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
时,其他线程可能又修改了base
或Cell
的值。因此,sum
的结果是**“调用期间多个时刻的值的混合”**,不是绝对精确的原子值。
- 无锁遍历