Java进阶篇之AtomicReferenceFieldUpdater:用不可变快照替换整份状态

上一篇用AtomicLongFieldUpdater把累计额度保存在对象字段中。如果配置同时包含"版本号、额度、开关",更新三个字段就会遇到新的问题:读线程需要看到一份完整配置,不能把新额度与旧开关拼在一起。

AtomicReferenceFieldUpdater 适合处理这种对象引用字段。把相关数据装进不可变对象,每次构造新快照,再通过CAS替换引用,读线程就能拿到同一份状态中的所有数据。

一、为什么多个volatile字段仍然会读到混合状态

假设配置原来是额度10、开关关闭,接下来需要切换成额度20、开关打开:

java 复制代码
volatile int limit = 10;
volatile boolean enabled = false;

void replace() {
    limit = 20;
    enabled = true;
}

两个字段各自具备volatile语义,但两次写入是两个独立动作。读线程在它们之间执行,就可能观察到limit=20、enabled=false。如果业务要求这两个值共同组成一个版本,这份组合就不完整。

可以把数据改成一个对象:

java 复制代码
record Settings(long revision, int limit, boolean enabled) {}
volatile Settings state = new Settings(0, 10, false);

引用替换只有一个提交点。读线程先取得Settings s = state,随后从s读取各字段,得到的就是同一份快照。

这里有两个相互配合的条件:引用更新具备正确的并发语义,快照对象发布后保持不变。如果取到快照以后还会有人修改它内部的数据,单次引用替换无法保证后续读取的一致性。

二、Updater怎样定位到对象中的引用字段

java 复制代码
private volatile Settings state = new Settings(0, 10, false);

private static final AtomicReferenceFieldUpdater<Config, Settings> UPDATER =
        AtomicReferenceFieldUpdater.newUpdater(
                Config.class, Settings.class, "state");

三个参数依次说明:字段属于哪个类、字段声明的引用类型、字段名称。泛型中的Config表示宿主对象,Settings表示被更新的引用值。

字段必须声明为volatile引用类型 。Updater的创建位置还必须拥有相应字段的访问权限。把创建代码放在持有私有字段的类内部,访问关系最清楚。newUpdater中的字段类型要与声明匹配,不能因为字段当前保存了某个子类对象,就传入那个子类的Class。

一个静态Updater可以操作多个Config实例;真正操作哪个对象,由get(config)、compareAndSet(config, before, after)的第一个参数决定。Updater本身不保存某一份业务配置。Java官方API

三、CAS替换的是引用,旧快照仍然保留

更新额度可以拆成三个步骤:

  1. 读取当前快照before。
  2. 根据它计算新快照after。
  3. 只有字段仍然指向before时,才把引用替换成after。
java 复制代码
for (;;) {
    Settings before = snapshot();
    Settings after = new Settings(
            Math.incrementExact(before.revision()),
            Math.addExact(before.limit(), delta),
            before.enabled());
    if (UPDATER.compareAndSet(this, before, after)) {
        return;
    }
}

假设两个线程同时读到版本0。线程A先提交版本1,线程B仍然携带版本0的引用,CAS就会失败。B回到循环顶部,读取版本1,重新计算版本2。这样,每次成功增加的额度都会基于最新状态计算,不会覆盖另一个线程已经提交的修改。

成功CAS是这次更新的提交点。新快照创建以后,CAS失败的对象不会成为当前配置;旧快照也没有被修改。只要读线程仍持有旧引用,它就能继续读取旧版本,直至自身处理结束。

四、完整实例:8个线程累加额度

下面的程序使用Java21。配置包含版本号、额度和开关,先启用配置,再让8个线程各增加100次额度。每个成功更新都会推进版本号,所有写入统一经过同一个Updater。

java 复制代码
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class ReferenceFieldDemo {
    record Settings(long revision, int limit, boolean enabled) {
        Settings {
            if (revision < 0 || limit < 0) throw new IllegalArgumentException();
        }
    }

    static final class Config {
        private volatile Settings state = new Settings(0, 10, false);
        private static final AtomicReferenceFieldUpdater<Config, Settings> UPDATER =
                AtomicReferenceFieldUpdater.newUpdater(Config.class, Settings.class, "state");

        Settings snapshot() { return UPDATER.get(this); }

        void addLimit(int delta) {
            if (delta <= 0) throw new IllegalArgumentException("delta must be positive");
            for (;;) {
                Settings before = snapshot();
                Settings after = new Settings(Math.incrementExact(before.revision()),
                        Math.addExact(before.limit(), delta), before.enabled());
                if (UPDATER.compareAndSet(this, before, after)) return;
            }
        }

        void enable() {
            UPDATER.updateAndGet(this, before -> before.enabled() ? before :
                    new Settings(Math.incrementExact(before.revision()), before.limit(), true));
        }
    }

    public static void main(String[] args) throws Exception {
        Config config = new Config();
        Settings oldSnapshot = config.snapshot();
        config.enable();
        try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
            Future<?>[] tasks = new Future<?>[8];
            for (int i = 0; i < tasks.length; i++) {
                tasks[i] = pool.submit(() -> {
                    for (int j = 0; j < 100; j++) config.addLimit(1);
                });
            }
            for (Future<?> task : tasks) task.get();
        }
        System.out.println("current=" + config.snapshot());
        System.out.println("old=" + oldSnapshot);

        Config probe = new Config();
        Settings actual = probe.snapshot();
        Settings equalCopy = new Settings(0, 10, false);
        Settings next = new Settings(1, 20, true);
        System.out.println("equals=" + actual.equals(equalCopy));
        System.out.println("sameReference=" + (actual == equalCopy));
        System.out.println("CAS(copy)=" + Config.UPDATER.compareAndSet(probe, equalCopy, next));
        System.out.println("CAS(actual)=" + Config.UPDATER.compareAndSet(probe, actual, next));
    }
}

保存为ReferenceFieldDemo.java,执行:

bash 复制代码
javac --release 21 ReferenceFieldDemo.java
java ReferenceFieldDemo

本例运行输出:

text 复制代码
current=Settings[revision=801, limit=810, enabled=true]
old=Settings[revision=0, limit=10, enabled=false]
equals=true
sameReference=false
CAS(copy)=false
CAS(actual)=true

额度从10增加到810,共提交800次累加;启用开关也提交一次,所以版本号为801。最早保存的oldSnapshot仍然是版本0,直观体现了"新引用接替旧引用,旧对象自身没有变化"。

Future.get()让主线程等待每个任务完成,并把任务异常传回主线程。若某个线程因为整数溢出等原因失败,程序会直接暴露异常,不会把未完成任务当作成功累加。

五、内容相等,不代表CAS比较通过

程序最后两个CAS很容易帮助我们理解引用比较:

java 复制代码
Settings actual = probe.snapshot();
Settings equalCopy = new Settings(0, 10, false);

这两个record内容相同,equals返回true,但它们是两个对象,actual == equalCopy返回false。compareAndSet按引用身份比较,也就是==,不会调用equals判断业务内容。

因此,用equalCopy作为期望值会失败;拿从字段中实际读出的actual作为期望值,且字段没有被其他更新改变时,CAS成功。期望值应保留读取时获得的对象引用,不能根据字段内容重新创建一个"看起来相同"的对象。Java官方API

六、落到业务代码时,四个细节要收紧

1. 读一次引用,再读取快照中的各项值

java 复制代码
Settings s = config.snapshot();
if (s.enabled()) {
    System.out.println(s.revision() + ":" + s.limit());
}

不要在同一项业务判断中反复调用snapshot()后分别取字段。两次调用之间可能已经发生更新,得到的版本与额度就可能来自不同快照。

2. record的不可变性需要看成员类型

本例成员都是基本类型。若新增List<String> endpoints,构造时应通过List.copyOf建立不可修改的列表,并保证元素自身也符合不可变要求。仅把一个可变List放进record,外部仍可能修改这个List的内容。

3. 更新函数不要发送消息或写数据库

updateAndGet的函数在竞争失败后可能再次执行,所以代码中的函数只计算新状态。日志计数、消息发送、数据库写入等外部动作应放在成功提交之后,并另外处理业务事务与重试语义。CAS成功并不会自动让外部系统的操作与它组成事务。Java官方API

4. 统一写入入口,避免重新挂回旧引用

示例把字段设为private,更新方法内部统一使用Updater。直接写字段会破坏这套更新纪律;保存旧快照后再把它写回,还会引入引用恢复为旧值的ABA问题。版本号只有实际参与设计与检查才有意义,增加一个数字字段不会自动让CAS变成带版本比较。

对于需要整体替换的中小型配置,不可变快照易于推导;对于高竞争下频繁更新的大对象,每次失败都要重新构造,分配与重试开销需要纳入选择依据。复杂的跨对象约束通常更适合用锁保护一个明确的临界区。

七、🧠 思维导图

#mermaid-svg-8j1iVAJxWYaC54do{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-8j1iVAJxWYaC54do .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8j1iVAJxWYaC54do .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8j1iVAJxWYaC54do .error-icon{fill:#552222;}#mermaid-svg-8j1iVAJxWYaC54do .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8j1iVAJxWYaC54do .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8j1iVAJxWYaC54do .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8j1iVAJxWYaC54do .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8j1iVAJxWYaC54do .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8j1iVAJxWYaC54do .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8j1iVAJxWYaC54do .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8j1iVAJxWYaC54do .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8j1iVAJxWYaC54do .marker.cross{stroke:#333333;}#mermaid-svg-8j1iVAJxWYaC54do svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8j1iVAJxWYaC54do p{margin:0;}#mermaid-svg-8j1iVAJxWYaC54do .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8j1iVAJxWYaC54do .cluster-label text{fill:#333;}#mermaid-svg-8j1iVAJxWYaC54do .cluster-label span{color:#333;}#mermaid-svg-8j1iVAJxWYaC54do .cluster-label span p{background-color:transparent;}#mermaid-svg-8j1iVAJxWYaC54do .label text,#mermaid-svg-8j1iVAJxWYaC54do span{fill:#333;color:#333;}#mermaid-svg-8j1iVAJxWYaC54do .node rect,#mermaid-svg-8j1iVAJxWYaC54do .node circle,#mermaid-svg-8j1iVAJxWYaC54do .node ellipse,#mermaid-svg-8j1iVAJxWYaC54do .node polygon,#mermaid-svg-8j1iVAJxWYaC54do .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8j1iVAJxWYaC54do .rough-node .label text,#mermaid-svg-8j1iVAJxWYaC54do .node .label text,#mermaid-svg-8j1iVAJxWYaC54do .image-shape .label,#mermaid-svg-8j1iVAJxWYaC54do .icon-shape .label{text-anchor:middle;}#mermaid-svg-8j1iVAJxWYaC54do .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8j1iVAJxWYaC54do .rough-node .label,#mermaid-svg-8j1iVAJxWYaC54do .node .label,#mermaid-svg-8j1iVAJxWYaC54do .image-shape .label,#mermaid-svg-8j1iVAJxWYaC54do .icon-shape .label{text-align:center;}#mermaid-svg-8j1iVAJxWYaC54do .node.clickable{cursor:pointer;}#mermaid-svg-8j1iVAJxWYaC54do .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8j1iVAJxWYaC54do .arrowheadPath{fill:#333333;}#mermaid-svg-8j1iVAJxWYaC54do .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8j1iVAJxWYaC54do .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8j1iVAJxWYaC54do .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8j1iVAJxWYaC54do .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8j1iVAJxWYaC54do .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8j1iVAJxWYaC54do .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8j1iVAJxWYaC54do .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8j1iVAJxWYaC54do .cluster text{fill:#333;}#mermaid-svg-8j1iVAJxWYaC54do .cluster span{color:#333;}#mermaid-svg-8j1iVAJxWYaC54do 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-8j1iVAJxWYaC54do .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8j1iVAJxWYaC54do rect.text{fill:none;stroke-width:0;}#mermaid-svg-8j1iVAJxWYaC54do .icon-shape,#mermaid-svg-8j1iVAJxWYaC54do .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8j1iVAJxWYaC54do .icon-shape p,#mermaid-svg-8j1iVAJxWYaC54do .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8j1iVAJxWYaC54do .icon-shape .label rect,#mermaid-svg-8j1iVAJxWYaC54do .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8j1iVAJxWYaC54do .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8j1iVAJxWYaC54do .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8j1iVAJxWYaC54do :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AtomicReferenceFieldUpdater
字段条件
volatile引用字段
类型与访问权限匹配
快照更新
读取旧引用
构造不可变新对象
CAS成功提交
失败重新计算
关键语义
按引用身份比较
旧快照保持不变
工程约束
读取一次快照
统一写入入口
更新函数无副作用

总结要点

AtomicReferenceFieldUpdater让对象内已有的volatile引用字段参与原子更新。把相关数据放进不可变快照,再替换引用,可以把多项配置组织成一个清晰的提交单元。

CAS比较引用身份。竞争失败后必须重新读取、重新计算;内容相等的新对象不能代替读取到的期望引用。读线程一次取得快照,再使用其中的字段,才能保持同一版本下的一致视图。

这套设计的重点是不可变数据、统一写入口与无副作用的计算函数。业务约束越复杂,越需要把快照范围和外部操作边界划清楚。

下一篇继续学习VarHandle,看看它如何统一访问字段与数组元素,以及不同内存访问模式该怎样选择。

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

相关推荐
Nathan__271 小时前
PHP面试题——情景应用
开发语言·php
geats人山人海1 小时前
现代c++2.4 虚继承
java·开发语言·c++
灯澜忆梦2 小时前
【面向对象编程c++】| 数组与函数
java·数据结构·c++
我命由我123452 小时前
Android 广播 - 组件通信
android·java·开发语言·android studio·android jetpack·android-studio·android runtime
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 安装 Nacos 注册中心与 Dubbo 监控中心
java·dubbo·交友
前端 贾公子2 小时前
LangGraph == 图的状态(State)管理 (中)
开发语言·数据库·python
李福春2 小时前
斗破Google ADK Java苍穹-第2境斗者-凝旋筑基-Java环境
java·开发语言·gemini·llmagent·深圳同盟·腾讯云架构师技术同盟·adk java
霸道流氓气质2 小时前
LLM API密钥管理与轮换策略:Spring Vault动态密钥、熔断限流协同与多Provider路由的 Java 生产级实现
java·后端·spring