上一篇用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,看看版本戳怎样帮助识别引用经历过的变化。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊