Java中的 AtomicReference类概览及实现方案
作者:探索java
一、引言
在 Java 开发中,常常需要构建无锁的应用程序,以实现高性能的并发控制。AtomicReference
就是它们中极其重要的一员。作为一种基于 CAS 机制实现的原实性工具,它在构建无锁队列、单例、上下文切换等场景中有着重要地位。
本文将随着一个体系化的进程,从基本概念到源码分析,展示 AtomicReference 在实际中如何做到 "精精不苦无锁同步"。
二、AtomicReference 概览
2.1 概念介绍
AtomicReference
是一个对应任意对象的原实性工具类,继承自 java.util.concurrent.atomic.AtomicReference<T>
,提供类似于基本型的 atomicXXX 类,但对象化。
它内部基于 Unsafe
的 CAS 操作,能够精确地实现与目标对象的引用替换。
2.2 类结构快览
public class AtomicReference<V> implements java.io.Serializable { private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; private volatile V value; // ... 重点方法看后续 }
类中主要是一个 volatile 字段 value,和基于 Unsafe 的 CAS 操作支持。
三、核心概念
3.1 CAS 原理
CAS(Compare-And-Swap)是一种无锁的原实性操作机制,其核心是把当前值和预期值比较,如果相等,则更新为新值。
实现简化为:
boolean compareAndSet(V expect, V update) { if (value == expect) { value = update; return true; } else { return false; } }
实际中这个操作是依赖硬件持有的原实性 CPU 指令来完成的,在 Java 中通过 Unsafe 来调用。
3.2 Unsafe 类
Unsafe 是一个本地级类,提供相当不安全的内核操作,包括直接操作内存,操作类的内部字段值等。
四、AtomicReference 的基础用法
4.1 创建和基本操作
public class AtomicReferenceDemo { public static void main(String[] args) { AtomicReference<String> ref = new AtomicReference<>("initial"); boolean updated = ref.compareAndSet("initial", "updated"); System.out.println("Update successful: " + updated); System.out.println("Current value: " + ref.get()); } }
输出:
Update successful: true
Current value: updated
五、源码深度解析
在深入理解 AtomicReference
的行为机制之前,有必要从源码级别进行一层层剖析。本章节将覆盖类结构、关键方法、内存语义以及 Unsafe.compareAndSwapObject
方法的底层实现。
5.1 类定义与字段分析
源码路径:java.util.concurrent.atomic.AtomicReference
public class AtomicReference<V> implements java.io.Serializable { private static final long serialVersionUID = -1848883965231344442L; // Unsafe 是用于执行底层 CAS 操作的核心类 private static final sun.misc.Unsafe unsafe = sun.misc.Unsafe.getUnsafe(); // 用于存储 value 字段的偏移量,在初始化时通过反射获取 private static final long valueOffset; // 关键字段,volatile 保证可见性 private volatile V value; static { try { // valueOffset 的计算,核心反射操作 valueOffset = unsafe.objectFieldOffset (AtomicReference.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } public AtomicReference(V initialValue) { value = initialValue; } public AtomicReference() { }
✅ 说明:
value
是被保护的实际引用,必须是volatile
,以确保在并发线程之间可见。valueOffset
是通过反射拿到的value
字段在对象内存结构中的偏移量。- 所有 CAS 操作都基于
valueOffset
。
5.2 核心方法解析
public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); } public final void set(V newValue) { value = newValue; } public final void lazySet(V newValue) { unsafe.putOrderedObject(this, valueOffset, newValue); } public final V get() { return value; } public final V getAndSet(V newValue) { while (true) { V current = get(); if (compareAndSet(current, newValue)) return current; } }
方法说明:
compareAndSet
: 基于 CAS 的原子条件更新,是核心方法。set
: 普通赋值操作,具备 volatile 语义。lazySet
: 有序写入,适用于延迟可见的情况,性能更优。getAndSet
: 实现方式为循环 CAS。
5.3 内存语义分析
volatile
语义:保证读写操作的可见性,防止指令重排。compareAndSet
的语义:具备 volatile 的读+写语义,同时含有内存屏障(full fence)。lazySet
语义:具备“store-store barrier”,只保证新值最终会被线程看到,但不强制立即生效。
内存屏障说明:
compareAndSet: LoadLoad + LoadStore + StoreStore + StoreLoad(全屏障) lazySet: StoreStore(写屏障)
这也是为什么在高并发但不要求严格即时可见性的场景下,lazySet
更高效。
5.4 Unsafe.compareAndSwapObject 深度剖析
该方法定义如下:
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
其是 native
方法,最终调用 JVM 内部封装的 CPU 指令实现(如 x86 架构下的 CMPXCHG)。其关键逻辑:
- 比较对象
o
中偏移量为offset
的字段值是否等于expected
- 如果相等,则用
x
替换原值,返回 true - 否则不做修改,返回 false
🚀 实质:此操作具备原子性,不会被线程上下文切换打断。
CPU 层级说明(以 x86 为例)
在底层,JVM 会借助 CPU 提供的原子指令实现上述逻辑,比如:
lock cmpxchg r/m32, r32 // 带锁的比较交换,保证总线级别原子
这确保了在多核环境中即使多个线程同时竞争更新,依然可以避免数据竞争。
5.5 JVM 字节码观察
我们通过 javap -c -v
查看 compareAndSet
调用层次:
javap -c -v java.util.concurrent.atomic.AtomicReference
输出片段如下:
public final boolean compareAndSet(java.lang.Object, java.lang.Object);
Code:
0: getstatic #16 // Field unsafe:Lsun/misc/Unsafe;
3: aload_0
4: getstatic #18 // Field valueOffset:J
7: aload_1
8: aload_2
9: invokevirtual #24 // Method sun/misc/Unsafe.compareAndSwapObject
12: ireturn
从字节码角度也能看出 compareAndSet 调用是对 Unsafe 对象方法的直接转发。
小结
AtomicReference
实现基于Unsafe
的 CAS 操作,绕过 synchronized 实现高性能原子更新。valueOffset
是通过反射获取的内存偏移,用于精确定位对象字段。compareAndSet
调用的是 JVM 层原子指令(cmpxchg
),确保并发安全。volatile
和lazySet
提供不同程度的内存可见性策略,需根据具体业务场景选择。
六、常见问题及解决方案
AtomicReference
虽然为无锁编程提供了便捷手段,但在实践中仍然存在若干值得注意的问题。特别是在高并发环境下,如果理解不当,可能会引入隐蔽的并发 bug。本章节总结了实际使用中遇到的典型问题及对应解决策略。
6.1 问题一:ABA 问题
问题描述:
CAS 操作基于对象的引用地址进行比较,若地址值相同则视为“未变”,这就带来一个典型并发陷阱:ABA 问题。
定义:
线程 A 读取变量为 A,线程 B 将其从 A 改为 B 再改回 A,线程 A 发现当前还是 A,于是操作成功,但其实已经发生过变化。
示例:
AtomicReference<String> ref = new AtomicReference<>("A"); Thread t1 = new Thread(() -> { String prev = ref.get(); // 读取到 A sleep(100); boolean success = ref.compareAndSet(prev, "C"); System.out.println("T1 CAS: " + success); // 可能为 true,但实际中间已经被改动过 }); Thread t2 = new Thread(() -> { ref.compareAndSet("A", "B"); ref.compareAndSet("B", "A"); }); t1.start(); t2.start();
风险:
虽然最终值为 "A",但实际上经历了变化。CAS 操作未感知这些中间变更,可能导致数据一致性问题。
解决方案:AtomicStampedReference
为了解决 ABA 问题,JDK 提供了 AtomicStampedReference
类。它不仅保存值,还维护一个版本号(stamp),每次更新都需同时更新 stamp,从而感知变量是否真正变化过。
示例:
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); Thread t1 = new Thread(() -> { int[] stamp = new int[1]; String prev = ref.get(stamp); sleep(100); boolean success = ref.compareAndSet(prev, "C", stamp[0], stamp[0] + 1); System.out.println("T1 CAS with stamp: " + success); }); Thread t2 = new Thread(() -> { int[] stamp = new int[1]; String curr = ref.get(stamp); ref.compareAndSet(curr, "B", stamp[0], stamp[0] + 1); ref.compareAndSet("B", "A", stamp[0] + 1, stamp[0] + 2); }); t1.start(); t2.start();
✅ 由于 stamp
不同,即使值回到 "A",也能检测到版本不一致,防止 ABA 问题。
6.2 问题二:引用本身原子,不代表对象状态原子
AtomicReference<T>
仅保证对象引用的更新是原子的,并不能保证引用对象内部的字段或状态是线程安全的。
示例:
class User { String name; int age; } AtomicReference<User> ref = new AtomicReference<>(new User()); ref.get().age++; // 非原子操作
风险:
如果多个线程同时修改 ref.get().age++
,依然可能引发竞态条件。
解决方案:
- 使用
AtomicReference<User>
实现整体替换(不可变类方案) - 或封装更新逻辑,通过 CAS 替换整个对象:
while (true) { User oldUser = ref.get(); User newUser = new User(); newUser.name = oldUser.name; newUser.age = oldUser.age + 1; if (ref.compareAndSet(oldUser, newUser)) break; }
6.3 问题三:频繁 CAS 导致性能下降
CAS 是乐观锁思想,但在高竞争场景下,多次失败会导致性能问题。
解决策略:
- 使用
LongAdder
、StampedLock
等替代方案 - 或者使用回退策略、自旋上限等优化手段
小结
问题 | 现象 | 解决方式 |
---|---|---|
ABA 问题 | 值恢复原样但中途被修改 | 使用 AtomicStampedReference |
引用更新原子但状态不安全 | 内部字段可能并发读写冲突 | 使用不可变对象整体替换 |
CAS 自旋性能差 | 多线程竞争失败导致 CPU 消耗上升 | 增加退避策略 / 限制自旋次数 |
在实际应用中,合理选择原子类并避免过度依赖单一机制是保障系统健壮性的关键。
到此这篇关于Java 中的 AtomicReference 类及其实现的文章就介绍到这了,更多相关Java AtomicReference 类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!