上一篇用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替换的是引用,旧快照仍然保留
更新额度可以拆成三个步骤:
- 读取当前快照
before。 - 根据它计算新快照
after。 - 只有字段仍然指向
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,看看它如何统一访问字段与数组元素,以及不同内存访问模式该怎样选择。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊