Java进阶篇之AtomicReference:一次替换引用,读到一份完整状态

上一篇用LongAccumulator处理可以合并的统计样本。配置更新面对的是另一类问题:版本号、超时值和路由列表需要一起变化,读取者也希望拿到同一份状态。把这些字段装进不可变对象,再整体替换引用,能让更新边界更容易看清。

本文适用于Java 21 ,围绕AtomicReference与不可变状态展开。后面的示例只验证引用比较、并发更新和旧快照保留,不把教学配置当成完整的生产配置中心。

一、先把相关字段放进同一个状态对象

假设一份配置包含revision、timeoutMillis和routes。若三个字段分别更新,读者可能正好在更新中途访问,读到"新版本号配旧超时值"。即便单个字段读写各自安全,三个字段之间的业务关系仍然需要协调。

可以让AtomicReference指向一个Settings对象。读者先调用一次get()取得引用,再从这一个对象读取相关字段;写者根据旧状态构造新对象,提交后让后续读取看到新对象。

这里有两层约定:引用整体替换 规定发布边界,对象不可变保证读者拿到的对象不会继续被其他线程修改。业务字段的一致性来自这两层配合。

二、CAS比较的是同一个引用

compareAndSet(expected, next)会把当前引用与expected进行身份比较。即使两个record的字段相等,只要它们是不同对象,也不能互相替代为expected。Java 21 AtomicReference文档明确使用==描述比较条件。

把更新步骤写出来,会更容易理解:

java 复制代码
Settings old;
Settings next;
do {
    old = state.get();
    next = new Settings(old.revision() + 1,
            old.timeoutMillis() + 1, old.routes());
} while (!state.compareAndSet(old, next));

当别人先一步替换了引用,这次提交失败,循环重新读取,再计算新状态。这个短片段沿用后面完整示例的Settings和state定义。

图中读者手中的旧配置卡仍保留原值,当前引用可以指向另一张配置卡。这种"快照"特指应用设计出的不可变对象,不表示AtomicReference自动复制了对象。

三、record还需要处理可变成员

record的组件字段为final,组件指向的对象仍可能可变。若routes直接保存外部ArrayList,别人继续向这个列表添加路由,旧配置也会跟着变化。

示例的紧凑构造器使用List.copyOf(routes)创建不可修改的列表表示。元素是不可变String,因此这个对象图满足这里需要的不可变约定。List.copyOf文档同时提醒:如果元素对象可变,列表本身不可修改也不会让元素变成不可变。

读取相关字段时也要坚持一次取引用:

java 复制代码
Settings snapshot = state.get();
int revision = snapshot.revision();
int timeout = snapshot.timeoutMillis();

若两行分别调用state.get(),中间可能发生更新,字段仍会来自不同对象。给快照一个局部变量,是很便宜也很清晰的约束。

四、完整示例:四个线程更新,旧对象保持原值

保存为AtomicReferenceDemo.java,用JDK21编译运行:

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    record Settings(int revision, int timeoutMillis, List<String> routes) {
        Settings {
            routes = List.copyOf(routes);
        }
    }

    public static void main(String[] args) throws Exception {
        Settings initial = new Settings(0, 1000, List.of("/health"));
        AtomicReference<Settings> state = new AtomicReference<>(initial);
        Settings heldSnapshot = state.get();
        Settings equalCopy = new Settings(0, 1000, List.of("/health"));
        System.out.println("equalByValue=" + initial.equals(equalCopy));
        System.out.println("casWithEqualCopy=" + state.compareAndSet(
                equalCopy, new Settings(1, 1001, initial.routes())));

        List<Future<?>> tasks = new ArrayList<>();
        try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
            for (int worker = 0; worker < 4; worker++) {
                tasks.add(pool.submit(() -> {
                    for (int i = 0; i < 100; i++) {
                        state.updateAndGet(old -> new Settings(
                                old.revision() + 1,
                                old.timeoutMillis() + 1,
                                old.routes()));
                    }
                }));
            }
            for (Future<?> task : tasks) {
                task.get();
            }
        }
        Settings current = state.get();
        System.out.println("currentRevision=" + current.revision());
        System.out.println("currentTimeout=" + current.timeoutMillis());
        System.out.println("heldSnapshotRevision=" + heldSnapshot.revision());
        System.out.println("heldSnapshotTimeout=" + heldSnapshot.timeoutMillis());
        try {
            current.routes().add("/debug");
            System.out.println("routesMutable=true");
        } catch (UnsupportedOperationException expected) {
            System.out.println("routesMutable=false");
        }
        if (current.revision() != 400 || current.timeoutMillis() != 1400) {
            throw new AssertionError("lost update");
        }
    }
}
bash 复制代码
javac AtomicReferenceDemo.java
java AtomicReferenceDemo

在本机JDK21.0.8中实际输出:

text 复制代码
equalByValue=true
casWithEqualCopy=false
currentRevision=400
currentTimeout=1400
heldSnapshotRevision=0
heldSnapshotTimeout=1000
routesMutable=false

equalByValue为true,说明record的值比较通过;casWithEqualCopy为false,说明CAS要求同一引用。这个失败发生在启动工作线程之前,结果不依赖调度碰巧出现的竞争。

四个线程各执行100次更新,最终revision为400,timeoutMillis为1400。主线程等待所有Future完成后再读取,构造函数保持的关系是timeoutMillis等于1000加revision。先前保留的heldSnapshot仍是0和1000,routes也拒绝直接添加元素。

这些输出证明了这个具体模型的行为,没有测量吞吐量、分配开销或高竞争下的延迟。

五、更新函数和业务副作用要分开

updateAndGet将重试逻辑封装起来。竞争发生时,更新函数可能再次执行,所以函数只负责由旧值计算新值。OpenJDK21实现可以看到它怎样在失败后继续尝试。

不要把发邮件、扣减外部额度、写入数据库或递增另一个共享计数器放进这个函数。即使引用最终只替换一次,函数执行次数也可能多于一次。需要外部动作时,先设计提交、幂等和失败处理协议。

场景 需要留意的边界
一份相关配置整体更新 将字段放进不可变对象,一次取引用读取
只有一个写者,直接发布新配置 set可以发布引用,仍需明确版本顺序
多个写者根据旧配置增量更新 用CAS或updateAndGet避免覆盖别人更新
修改对象内部可变字段 AtomicReference不提供内部字段保护
两个AtomicReference一起变化 两次原子替换没有自动组成一个事务
希望发现A到B再回A的历史变化 单凭引用CAS不记录经历,需要额外版本协议

不可变对象每次更新会创建新状态。大对象、频繁写入与长时间持有旧状态都可能影响分配和内存占用,应通过实际负载评估;不要从API名称推断它一定更快。

六、🧠 思维导图

#mermaid-svg-OtGdFFnqEcbadaBA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OtGdFFnqEcbadaBA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OtGdFFnqEcbadaBA .error-icon{fill:#552222;}#mermaid-svg-OtGdFFnqEcbadaBA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OtGdFFnqEcbadaBA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OtGdFFnqEcbadaBA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OtGdFFnqEcbadaBA .marker.cross{stroke:#333333;}#mermaid-svg-OtGdFFnqEcbadaBA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OtGdFFnqEcbadaBA p{margin:0;}#mermaid-svg-OtGdFFnqEcbadaBA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OtGdFFnqEcbadaBA .cluster-label text{fill:#333;}#mermaid-svg-OtGdFFnqEcbadaBA .cluster-label span{color:#333;}#mermaid-svg-OtGdFFnqEcbadaBA .cluster-label span p{background-color:transparent;}#mermaid-svg-OtGdFFnqEcbadaBA .label text,#mermaid-svg-OtGdFFnqEcbadaBA span{fill:#333;color:#333;}#mermaid-svg-OtGdFFnqEcbadaBA .node rect,#mermaid-svg-OtGdFFnqEcbadaBA .node circle,#mermaid-svg-OtGdFFnqEcbadaBA .node ellipse,#mermaid-svg-OtGdFFnqEcbadaBA .node polygon,#mermaid-svg-OtGdFFnqEcbadaBA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OtGdFFnqEcbadaBA .rough-node .label text,#mermaid-svg-OtGdFFnqEcbadaBA .node .label text,#mermaid-svg-OtGdFFnqEcbadaBA .image-shape .label,#mermaid-svg-OtGdFFnqEcbadaBA .icon-shape .label{text-anchor:middle;}#mermaid-svg-OtGdFFnqEcbadaBA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OtGdFFnqEcbadaBA .rough-node .label,#mermaid-svg-OtGdFFnqEcbadaBA .node .label,#mermaid-svg-OtGdFFnqEcbadaBA .image-shape .label,#mermaid-svg-OtGdFFnqEcbadaBA .icon-shape .label{text-align:center;}#mermaid-svg-OtGdFFnqEcbadaBA .node.clickable{cursor:pointer;}#mermaid-svg-OtGdFFnqEcbadaBA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OtGdFFnqEcbadaBA .arrowheadPath{fill:#333333;}#mermaid-svg-OtGdFFnqEcbadaBA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OtGdFFnqEcbadaBA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OtGdFFnqEcbadaBA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OtGdFFnqEcbadaBA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OtGdFFnqEcbadaBA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OtGdFFnqEcbadaBA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OtGdFFnqEcbadaBA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OtGdFFnqEcbadaBA .cluster text{fill:#333;}#mermaid-svg-OtGdFFnqEcbadaBA .cluster span{color:#333;}#mermaid-svg-OtGdFFnqEcbadaBA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OtGdFFnqEcbadaBA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OtGdFFnqEcbadaBA rect.text{fill:none;stroke-width:0;}#mermaid-svg-OtGdFFnqEcbadaBA .icon-shape,#mermaid-svg-OtGdFFnqEcbadaBA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OtGdFFnqEcbadaBA .icon-shape p,#mermaid-svg-OtGdFFnqEcbadaBA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OtGdFFnqEcbadaBA .icon-shape .label rect,#mermaid-svg-OtGdFFnqEcbadaBA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OtGdFFnqEcbadaBA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OtGdFFnqEcbadaBA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OtGdFFnqEcbadaBA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AtomicReference
状态组织
相关字段放一起
不可变对象
读取方式
get取得一次引用
旧快照保持原值
更新方式
CAS比较同一引用
函数更新允许重试
业务边界
内部成员也要安全
外部副作用单独协调

七、总结

总结要点

状态先组织,引用再发布。把有关联的字段放进一个不可变对象,读者一次拿到引用,才能保留这份状态里的关系。

CAS比较对象身份。字段相等的新对象无法替代实际读到的旧引用;提交失败后重新计算,让更新接上最新状态。

不可变约定覆盖整个对象图。record和不可修改列表各解决一部分问题,成员内容及外部副作用仍需要明确设计。

下一篇继续学习AtomicStampedReference,看看版本戳怎样帮助识别引用经历过的变化。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

相关推荐
专业程序开发源1 小时前
springboot简历管理系统81389-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-前后端分离开发模式与工程拆分
java·开发语言
HEJOO92 小时前
指针的算术运算详解
java·数据结构·算法
sunshine22 girl2 小时前
Java学习六 Java常见API-2--String-3-字符串的截取替换和其他方法
java·开发语言·学习
EatFan2 小时前
从“框架混战“到“运行时收敛“:2026 年 AI Agent 开发框架的三条路线之争
java·数据库·人工智能·多智能体·ai agent·mcp·agent 框架
小蒜学长2 小时前
基于SpringBoot的佳新超市管理系统设计与实现系统(代码+数据库+LW)
java·数据库·spring boot·后端·佳新超市管理系统
sunshine22 girl3 小时前
Java学习六 Java常见API-1-Radom随机数
java·开发语言·学习
wangbing11253 小时前
使用jps jstack排查系统问题
java
于指尖飞舞4 小时前
java面试题总结
java·开发语言