在并发编程里很多问题本质上是多线程同时改一个共享变量怎么保证不出错。早期我们用synchronized加锁简单粗暴但锁一旦竞争激烈线程阻塞、唤醒的开销立刻让人头疼。后来 Java 提供了java.util.concurrent.atomic包里面一堆原子类用 CAS 无锁方案把性能拉高了好几个档次。而AtomicReference就是专门用来原子更新对象引用的那一个。我最早接触它是在做本地缓存热更新时多个线程并发替换一个配置对象用synchronized总感觉太重换成AtomicReference后代码清爽很多性能也稳。这篇就把 AtomicReference 的原理、实战、以及面试里那些弯弯绕绕一次讲透。1. 从原子类家族说起AtomicInteger、LongAdder、AtomicReference1.1 原子类到底解决了什么并发难题想象一下这样一个场景一个计数器多个线程同时执行count。这行代码看起来是一条命令但在 CPU 眼里其实是三步——读取 count 的当前值把这个值加一再把新值写回内存。如果两个线程同时读到同一个旧值各自加一后写回最终 count 只增加了一次这就是典型的并发竞争问题。volatile能解决可见性保证一个线程修改后其他线程立刻能看到最新值但它解决不了读-改-写这个复合操作的原子性。synchronized能保证原子性可一旦多个线程同时竞争没有抢到锁的线程就要阻塞、挂起线程切换和唤醒的成本在低并发场景下甚至可能超过业务本身的开销。原子类走的是第三条路自旋 CAS volatile。没有锁没有阻塞线程反复尝试比较-交换成功了就返回失败了就重来。对于短小精悍的更新操作来说这种方式在乐观并发场景下表现远好于加锁而且代码写出来也比锁直观得多。1.2 AtomicInteger 到 LongAdder 的演进AtomicInteger是原子类里最典型的代表内部维护一个volatile int value通过compareAndSet实现原子自增。它的特点是在低竞争下性能极佳但高并发下大量线程同时 CAS只有一个能成功其余全部自旋重试CPU 空转的浪费很可观。LongAdder就是冲着高并发优化来的。它在内部维护了一个基础值base和一个Cell[]数组每个线程更新时会分散到不同的 Cell 上最后汇总时把 base 和所有 Cell 加起来。核心思想是分而治之——把单一竞争热点拆成多个热点线程各抢各的冲突概率骤减。这跟 ConcurrentHashMap 在 JDK 8 里用数组分段降低锁竞争是一个思路。所以在实际开发中统计类需求我一般这样选并发量低、实时性强AtomicLong/AtomicInteger简单直接。高并发写入、允许稍后汇总LongAdder吞吐量明显更高。需要返回精确的更新值比如序列号AtomicLong因为addAndGet能返回本次操作后的值而 LongAdder 没有这个能力。1.3 AtomicReference 在原子类家族中的特殊定位AtomicInteger、AtomicLong 只能包装数值类型AtomicBoolean 只能包装布尔值。但并发编程中很多共享资源的状态不是一个数字而是一个对象——配置对象、连接池里的连接、缓存里的节点、甚至是一个 immutable 的 View 对象。AtomicReferenceT就是干这个的它把整个 T 类型的对象引用当作一个整体来做原子更新。内部维护一个volatile V value更新时用 CAS 比较引用是否还是预期的旧引用是就替换成新引用。需要特别注意的是它保证的是引用本身的原子性不是对象内部的字段不可变。也就是说如果 T 是可变的其他线程仍然可能在 CAS 成功后修改这个对象的内部状态。所以配合 AtomicReference 使用的对象最佳实践是不可变对象或者至少保证内部状态线程安全。2. AtomicReference 核心原理CAS 与内存屏障2.1 CAS 是怎么做到无锁也能保证安全的CAS 的全称是 Compare And Swap比较并交换。它做的事情可以用下面这段伪代码理解public boolean compareAndSet(V expectedValue, V newValue) { if (this.value expectedValue) { this.value newValue; return true; } return false; }但真正的 CAS 不是 Java 方法而是 CPU 提供的一条硬件指令。比如 x86 平台上的cmpxchg指令它会在一次原子操作里完成比较和交换两个动作期间总线加锁其他核心无法插入访问。正因如此整个检查-更新过程是不可分割的不可能出现检查完还没更新被另一个线程改了的窗口期。在 JDK 里AtomicReference.compareAndSet最终会走到Unsafe.compareAndSwapObject这是一个 native 方法通过 JNI 调入 JVM 运行时最终映射到 CPU 指令。整个链路是Java 方法 → Unsafe → JNI → JVM 内联函数 → CPU 指令2.2 自旋、循环重试与实际性能CAS 本身只尝试一次失败了不会自动重试。但AtomicReference.getAndSet、getAndUpdate这类方法内部会用一个do/while循环不断自旋直到成功public final V getAndUpdate(UnaryOperatorV updateFunction) { V prev, next; do { prev get(); next updateFunction.apply(prev); } while (!compareAndSet(prev, next)); return prev; }每次循环都重新读当前值算出新值再 CAS。如果一直被其他线程抢先就一直空转。这也解释了为什么高竞争场景下 CAS 的性能会劣化——CPU 空转本身是有成本的多个线程同时自旋还会加剧缓存行的争用。这里有一个容易被忽略的性能陷阱多个 AtomicReference 共享同一个缓存行时会触发伪共享。CPU 从内存加载数据是按缓存行通常 64 字节加载的如果两个原子变量恰好落在同一个缓存行一个线程更新其中一个变量会导致另一个变量所在的缓存行整体失效其他线程访问它时不得不重新从内存读取。解决方式通常是用Contended注解或者手动填充 padding把热点变量隔开到不同缓存行。LongAdder 的 Cell 数组里面就做了这样的处理。2.3 内存可见性AtomicReference 的 volatile 关键词AtomicReference 内部维护的 value 字段被声明为volatile这是它的可见性保证。volatile 有两个作用对一个 volatile 变量的写操作会立即刷新到主内存并且在该写操作之前的所有普通写操作对其他变量的修改也会一起刷新。对一个 volatile 变量的读操作会从主内存重新读取并且在该读操作之后的所有普通读操作都不会使用过期的 CPU 缓存。这就是 JMMJava 内存模型中的 happens-before 规则对一个 volatile 变量的写先行发生于后续对该变量的读。所以用 AtomicReference 更新引用后其他线程调用get()一定能看到最新引用不用额外再去加锁或加 volatile 修饰。不过要强调一点volatile 保证的是引用可见性不是对象内部状态的可见性。如果你 set 了一个新对象引用而新对象里的字段值是在 set 之前写入的那没问题因为 volatile 写会连带刷新之前的普通写。但如果你 set 之后再去修改对象内部字段那就没有任何保证其他线程看到的可能是半新半旧的中间态。3. AtomicReference 实操从入门到进阶3.1 基础用法与构造方法使用方式非常直白AtomicReferenceString ref new AtomicReference(初始值); String current ref.get(); boolean isOk ref.compareAndSet(初始值, 新值);构造时有三个常见选择new AtomicReference(); // value 初始为 null new AtomicReference(initial); // 直接用初始对象另一个常用方法是getAndSet它原子地把当前引用替换成目标值然后返回旧值String old ref.getAndSet(新值);这个操作不需要预期值所以永远不会失败在实现版本替换状态轮换这类逻辑时很方便。还有 JDK 8 之后引入的getAndUpdate、updateAndGet、getAndAccumulate、accumulateAndGet它们接受函数式接口可以在原子操作里完成更复杂的更新逻辑。最典型的是把多个字段的局部更新压缩进一次 CASref.updateAndGet(old - { if (old null) return new Config(1); return new Config(old.version 1); });3.2 实战场景一单例模式的线程安全初始化很多人知道双重检查锁DCL能实现线程安全的懒加载单例却常常忘记给单例字段加 volatile。实际上 DCL 的重排序问题就出在半初始化对象上instance new Instance()不是一条原子指令它包含分配内存、初始化对象、把引用指向内存三步CPU 和编译器可以在不影响单线程语义的前提下重排后两步。如果另一个线程在这期间读到了未初始化的引用就会拿到一个残废对象。用 AtomicReference 可以换一种思路避免这些问题public class Singleton { private static final AtomicReferenceSingleton INSTANCE new AtomicReference(); private Singleton() {} public static Singleton getInstance() { Singleton current INSTANCE.get(); if (current ! null) { return current; } current new Singleton(); if (INSTANCE.compareAndSet(null, current)) { return current; } else { return INSTANCE.get(); } } }第一次 get 时可能多个线程同时发现为空各自 new 一个但只有一个 CAS 能成功其余线程 CAS 失败后读取别人创建好的实例。这种方式没有锁代码更短性能也比 DCL 好一些。当然在实战中我更推荐枚举单例或静态内部类但 AtomicReference 的实现方式在面试中能加分因为它体现了对 CAS 原子操作的深刻理解。3.3 实战场景二无锁栈与无锁队列的实现思路CAS 的一大应用场景是实现无锁数据结构。这里举一个线程安全的无锁栈核心就是用 AtomicReference 持有栈顶节点public class AtomicStackE { private final AtomicReferenceNodeE top new AtomicReference(); private static class NodeE { final E item; final NodeE next; Node(E item, NodeE next) { this.item item; this.next next; } } public void push(E item) { NodeE newHead new Node(item, null); NodeE oldHead; do { oldHead top.get(); newHead.next oldHead; } while (!top.compareAndSet(oldHead, newHead)); } public E pop() { NodeE oldHead; NodeE newHead; do { oldHead top.get(); if (oldHead null) { return null; } newHead oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); return oldHead.item; } }push 的逻辑很清晰拿到当前栈顶把新节点指向它然后 CAS 更新栈顶为 newHead。如果 CAS 失败说明期间有别的线程改过栈顶那就重新读、重新指再试。pop 同理只是把栈顶往后挪一个节点。这个栈用到的套路和 ConcurrentLinkedQueue 内部实现非常相似。区别在于队列有两个指针head 与 tail更新两个指针时无法用单个 AtomicReference 实现所以通常用AtomicReferenceNode指向一个持有 head 和 tail 的状态对象或者干脆用多个 AtomicReference 字段并用版本号保护。理解无锁栈再去看 ConcurrentLinkedQueue 源码会顺畅很多。3.4 实战场景三缓存热更新与不可变对象缓存里最常见的需求是周期性刷新配置但读取时永远拿到一个完整一致的快照。如果直接用 volatile 字段已经够用但如果你想在 CAS 基础上做条件更新比如只在版本号比当前新时才替换用 AtomicReference 就非常顺手。把配置类设计成不可变对象所有字段用 final 修饰public final class AppConfig { public final int timeout; public final int maxRetries; public AppConfig(int timeout, int maxRetries) { this.timeout timeout; this.maxRetries maxRetries; } }然后持有引用private final AtomicReferenceAppConfig configRef new AtomicReference(new AppConfig(1000, 3)); public void refresh(AppConfig newConfig) { AppConfig oldConfig; do { oldConfig configRef.get(); if (newConfig.timeout oldConfig.timeout) { return; // 不符合刷新条件直接放弃 } } while (!configRef.compareAndSet(oldConfig, newConfig)); }读取线程只需要AppConfig config configRef.get(); int timeout config.timeout;只要不修改 config 对象内部字段每次拿到的都是完整快照不会看到 timeout 和 maxRetries 来自不同版本这种错乱状态。这正是不可变对象 原子引用的经典组合网上常说的 Copy-On-Write 思想在并发场景里也是这个套路。4. 常见问题与踩坑实录4.1 ABA 问题不能只看结果还要看过程CAS 的缺点是它只比较当前值是否等于预期值不关心这个值是否曾经被改成别的后来又改回来。举个例子线程1 读取 ref A 线程2 把 ref 改成 B然后又改回 A 线程1 执行 CAS(A, C) 成功问题是线程1 认为 A 一直没变但实际中间被 B 插了一脚。如果它依赖的是A 没被改过这个假设来做决策就会出错。典型的案例是链表栈如果在线程1 暂停期间栈顶从 A 变成 B 又变回 A但 B 的 next 已经被改变线程1 基于旧 top 做 CAS可能把已经被改变的链表结构恢复成旧结构丢失节点。解决 ABA 问题的标准方案是AtomicStampedReference或AtomicMarkableReference。前者在内部同时维护对象引用和整数版本号每次修改引用时版本号也一起增加比较时不仅比较引用还比较版本号。这就好比你判断一个人是不是那个人不仅看脸还要看身份证号身份证号变了就说明中间发生过变更。用 AtomicStampedReference 改造上面的无锁栈非常直接private final AtomicStampedReferenceNodeE top new AtomicStampedReference(null, 0); public void push(E item) { NodeE newHead new Node(item, null); int[] stampHolder new int[1]; NodeE oldHead; do { oldHead top.get(stampHolder); newHead.next oldHead; } while (!top.compareAndSet(oldHead, newHead, stampHolder[0], stampHolder[0] 1)); }每一次 CAS 都会让版本号加一这样即使引用值绕了一圈回到 A版本号也不会回去ABA 就被识别出来了。4.2 自旋开销过大AtomicReference 的性能边界AtomicReference 在竞争不激烈时性能极好但一旦线程数量远超 CPU 核心数大量线程进入自旋状态每个线程都在反复循环执行 CASCPU 占用率会飙升甚至出现为了不用锁反而比用锁更慢的怪圈。实测在高竞争场景下自旋 CAS 的吞吐量会出现明显下降甚至低于 synchronized。遇到这种情况我的建议是如果更新逻辑短且简单并发竞争中等AtomicReference 仍然是首选。如果能接受最终一致性聚合优先用 LongAdder 那套分段思想或把大对象拆成多个小原子变量分散热点。如果更新逻辑本身耗时较长比如依赖 IO、数据库CAS 自旋的代价会成倍放大这时候锁反而更合理因为锁至少会把等待线程挂起而不是空占 CPU。判断方式很简单压测对比。在业务环境里用同参数、同数据量、不同并发线程数跑一个更新的性能测试看吞吐量和 CPU 占用率就能对 AtomicReference 的适用范围有个明确边界。4.3 不要把 AtomicReference 当锁用复合校验还是需要同步AtomicReference 的 CAS 只能保证替换引用这一步的原子性它不能保证多个字段之间的约束关系。比如一个转账系统审计人员希望同时更新 balance 和 lastTxTime两个字段必须保持同一次事务的维度一致。如果用 AtomicReference 只替换其中一个字段另一个字段还是旧值CAS 成功也没有意义。这种情况下要不就把 balance 和 lastTxTime 封装成一个不可变 value 对象用 AtomicReference 一次性替换要不就老老实实加锁。另外AtomicReference 也没有等待能力。synchronized或ReentrantLock可以让线程在条件不满足时阻塞等待等另一个线程完成后被唤醒。AtomicReference 只能返回成功或失败无法让调用方挂起。如果需要依赖某个引用从 null 变成非 null 然后继续执行用CompletableFuture或者CountDownLatch更合适。还有一个很多人会踩的坑使用updateAndGet时updateFunction 必须无副作用并且不要去依赖外部状态的实时性。因为 updateFunction 可能在 CAS 失败后执行多次每次入参的值都可能是新的。如果 updateFunction 本身改了外部变量或者打印了日志容易造成重复执行的问题排查起来还特别隐蔽。4.4 面试高频点从 AtomicReference 发散开的并发知识面试官问 AtomicReference常常不只是问 API而是把它当引子逐步深入并发编程的其他核心。常见的问题链是这样展开的AtomicReference 是怎么保证原子性的答CAS volatileCAS 依赖 CPU 指令。CAS 的底层指令是哪个答x86 的 cmpxchgARM 的 LDREX/STREX不同平台不同。CAS 会一直自旋吗答JVM 内部对某些 CAS 操作会有自适应自旋超过阈值后会挂起或走系统调用具体看 JVM 实现和锁升级机制。CAS 有什么缺点答ABA、自旋开销、只能保护单一引用变量。怎么解决 ABA答AtomicStampedReference 或者 AtomicMarkableReference。AtomicReference 和 AtomicInteger 的适用场景差异在哪答一个针对对象引用一个针对数值底层原理一致。LongAdder 相对 AtomicLong 的优势是什么答分段累计降低热点竞争但 sum 结果不是强一致快照。volatile 和 CAS 有什么区别答volatile 保证可见性和有序性不能保证复合操作原子性CAS 是乐观锁实现能保证单一变量更新的原子性。CAS 与 AQS 的关系答AQS 底层用 CAS 修改 state 字段加锁抢锁过程就是 CAS 自旋的体现。JDK 里很多锁工具都是基于 AQS而 AQS 的核心状态位正是通过 AtomicInteger 的 CAS 来修改。把这条链都想明白不仅 AtomicReference 通了整个 JUC 并发包的骨架也搭起来了。4.5 一个真实案例从 AtomicReference 到性能问题的排查之前我在一个分布式任务调度项目里遇到过一个问题节点心跳状态用一个volatile boolean记录每个心跳线程每 5 秒更新一次状态管理线程会频繁读取这个状态来决定是否重新分配任务。低负载时一切正常但节点数增加到数百后管理线程的检查逻辑频繁出现状态错乱的现象。最终定位后发现多个心跳线程在抢一个 AtomicReference 的更新权导致自旋严重、CPU 波动剧烈进而拖慢了整个 JVM 的响应。修复方式不是换锁而是把心跳状态从单一引用拆成多个独立的原子状态例如lastHeartbeatTime用AtomicLongregistered用AtomicBoolean并把判断是否需要重新分配的逻辑改成基于时间戳比较。这样热点从一个对象引用分散成两个独立变量竞争大幅下降问题就解决了。这个案例想表达的是AtomicReference 用起来简单但用之前一定要评估它的竞争烈度和更新频率。不要因为它是原子类就默认它一定快。它无锁是有前提的——竞争得低操作得短一旦前提不成立它反而可能成为新的性能瓶颈。5. 从 AtomicReference 出发再聊聊并发选型的经验5.1 什么时候选 AtomicReference什么时候选锁我自己现在做并发设计时会先问三个问题更新的对象是什么只有一个引用吗如果是AtomicReference 非常适合。更新频率高吗竞争严重吗如果频率极高且线程数很多考虑分段或锁。更新后的操作需要等待吗如果线程 A 更新后线程 B 必须等待某个条件才能继续锁或显式并发工具更适合。下面这张对比表是我自己整理的日常选型时直接套用维度AtomicReferencesynchronized / ReentrantLock保护粒度单个引用变量代码块或方法竞争高时表现自旋消耗 CPU阻塞挂起CPU 占用低复合字段约束需要封装成整体对象可以直接锁多字段变更等待条件不支持支持 wait/notify、Condition可重入天然无锁锁可重入适用场景高频读、低竞争写、单一引用替换复杂逻辑、需要原子修改多个变量结论是AtomicReference 适合做轻量级的变量替换不适合做重量级的事务性操作。选错方案的代码往往初期跑得很欢一旦进入高并发压测阶段问题就开始浮现。5.2 不可变对象与 AtomicReference 是最佳搭档AtomicReference 本身只管理引用的更新但如果被引用的对象是可变的CAS 成功并不等于安全。举一个反例一个缓存对象里面有 ArrayList两个线程通过 AtomicReference 拿到了同一个缓存对象其中一个线程往 ArrayList 里 add 数据另一个线程正在遍历就会抛 ConcurrentModificationException。解决办法是不去修改已有对象而是新建一个改好的对象再替换引用。这就是 Immutable Object CAS 的组合也是并发编程里非常推荐的模式。每次更新都创建一个新的对象对象字段全部 finalCAS 成功后才让所有人看到新版本。旧对象没人引用后自然被 GC 回收。这种模式读线程永远无锁写线程通过 CAS 互斥性能和安全都能兼顾。CopyOnWriteArrayList 的写时复制思想跟这个完全一致修改时不是去动原数组而是复制一份新数组改好再替换。AtomicReference 里经常搭配的状态对象也是同样的设计思路。5.3 需要补充的一点严格限制可变对象的使用AtomicReference 能解决引用替换的原子性但不要因此忽略对象本身的可变性风险。如果实在要用可变对象最稳妥的方式是让所有对内部字段的修改也必须经过同一个 AtomicReference 的保护。也就是说把修改这个行为统一收敛到少数几个更新方法里由 CAS 保证每次更新都是全量替换而不是局部修改。时间长了你会发现与其小心翼翼地控制可变对象的并发修改不如直接改成不可变对象省心得多——精神和体力上的成本都更低。6. 最后的经验分享AtomicReference 常见错误速查最后整理一份我踩过和见过别人踩的坑希望对你有帮助常见错误问题说明正确做法用 AtomicReference 包裹可变对象并在外部直接改内部字段CAS 只保护引用保护不了对象内部状态改为不可变对象或通过 CAS 整体替换把 CAS 当乐观锁无限循环使用高竞争下 CPU 自旋开销大甚至拖垮系统评估竞争量必要时换 LongAdder 或锁忽略 ABA 问题只比较引用无法感知中间变化使用 AtomicStampedReference 带版本号updateAndGet 的函数里做打印或外部赋值函数会重试多次导致副作用重复执行保持纯函数无副作用set 与 getAndSet 混用忘记 getAndSet 会返回旧值返回值里隐藏了旧状态容易误用根据需求选择 set 还是 getAndSet在多字段一致性场景用多个 AtomicReference多个引用各自 CAS 无法形成整体原子性封装成一个不可变对象用单个原子引用替换认为 AtomicReference 比 synchronized 一定快无锁只在低竞争下高效做压测对比后再选型AtomicReference 使用门槛低但要用对地方。它不是一个万能的锁替代品而是一个精细的工具适合在单变量替换、低竞争更新、状态快照切换这些场景里大放异彩。理解了 CAS 的原理、ABA 的陷阱、不可变对象的配合你才算真正掌握了它。以后再看到并发问题先别急着上锁想想能不能用原子引用 不可变对象很多场景真的能优雅不少。