Java进阶篇之AtomicLongFieldUpdater:把累计额度留在对象字段中

上一篇用AtomicIntegerFieldUpdater处理对象里的名额。这次把场景换成上传额度:一个账户最多累计接收100亿字节,多个上传任务同时申请额度。每次申请都要先判断容量,再把申请量加入累计值。

AtomicLongFieldUpdater可以直接更新对象已有的long字段。难点落在两个地方:并发时怎样把判断和提交连接起来,以及接近long上限时怎样避免算术溢出。下面从这两个问题推导实现,示例使用Java21。

一、结构介绍:更新器共享,额度各自保存

Budget保存两份数据:不可变的额度上限limit,以及不断增长的已用值used。更新器作为类的静态成员保留一份,每次操作都传入具体的Budget对象。

java 复制代码
private static final AtomicLongFieldUpdater<Budget> USED =
        AtomicLongFieldUpdater.newUpdater(Budget.class, "used");
private volatile long used;
private final long limit;

这份结构中,更新器负责访问规则,对象持有状态。账户A申请额度,只会修改A的used;账户B仍然保留自己的累计值。更新器里的泛型指定目标类,第二个参数指定字段名。Oracle API

字段必须是volatile long,不能换成Long,也不能移除volatile。把更新器放在持有私有字段的类中创建,可以让字段保持封装,外部通过业务方法申请额度。

二、先算剩余额度,再构造新值

假设当前已经用了90亿字节,又来一个20亿字节的申请。我们要拒绝它,同时保留原来的90亿。下面这种顺序有问题:

java 复制代码
long next = used + bytes;
if (next <= limit) {
    used = next;
}

两个线程能够同时读到相同的used,各自判断通过,再互相覆盖结果。即使字段声明为volatile,整个"读取、相加、判断、写入"也没有合成一个原子操作。

还有一个容易忽略的算术问题:long相加超过最大值后会回绕为负数,next <= limit反而可能成立。容量检查必须在安全的算术范围内完成。

本例维护的不变量是:

text 复制代码
0 <= used <= limit <= Long.MAX_VALUE
bytes > 0

在这个前提下,limit - current始终是非负且可表示的long。先比较申请量与剩余额度:

java 复制代码
if (bytes > limit - current) return false;
long next = current + bytes;

能执行到第二行,就已经证明current + bytes <= limit。此时相加既不会超额,也不会溢出。这个证明依赖used从0开始、所有后续写入都经过同一套规则;代码里不再暴露任意设置used的接口。

三、CAS把容量判断变成条件提交

线程读到current后,其他线程仍可能先提交新累计值。因此,最后一步使用CAS:只有字段仍然等于本次读取的current,才把next写进去。

java 复制代码
while (true) {
    long current = USED.get(this);
    if (bytes > limit - current) return false;
    long next = current + bytes;
    if (USED.compareAndSet(this, current, next)) return true;
}

以两个任务同时申请为例,A和B都先读到60亿,各自申请30亿。A成功把字段改为90亿,B用"预期60亿"提交时失败。B下一轮读到90亿,发现只剩10亿,直接返回false。

CAS成功的瞬间,就是本次额度预留生效的时刻。

失败后要重新读取并重新判断。沿用上一轮计算出的next,会把已经变化的剩余额度当成旧额度使用。

四、完整示例:16个申请争取10份额度

每个任务申请10亿字节,总额度100亿。任务的执行顺序由线程调度决定,成功数量由容量约束决定。

保存为LongFieldUpdaterDemo.java:

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;

public class LongFieldUpdaterDemo {
    static final class Budget {
        private static final AtomicLongFieldUpdater<Budget> USED =
                AtomicLongFieldUpdater.newUpdater(Budget.class, "used");
        private volatile long used;
        private final long limit;

        Budget(long limit) {
            if (limit < 0) throw new IllegalArgumentException("limit < 0");
            this.limit = limit;
        }

        boolean tryReserve(long bytes) {
            if (bytes <= 0) throw new IllegalArgumentException("bytes <= 0");
            while (true) {
                long current = USED.get(this);
                if (bytes > limit - current) return false;
                long next = current + bytes;
                if (USED.compareAndSet(this, current, next)) return true;
            }
        }

        long used() { return USED.get(this); }
    }

    public static void main(String[] args) throws Exception {
        Budget main = new Budget(10_000_000_000L);
        int successes = 0;
        try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
            CountDownLatch start = new CountDownLatch(1);
            List<Future<Boolean>> futures = new ArrayList<>();
            for (int i = 0; i < 16; i++) {
                futures.add(pool.submit(() -> {
                    start.await();
                    return main.tryReserve(1_000_000_000L);
                }));
            }
            start.countDown();
            for (Future<Boolean> future : futures) {
                if (future.get()) successes++;
            }
        }
        System.out.println("successes=" + successes);
        System.out.println("used=" + main.used());
        System.out.println("extra=" + main.tryReserve(1));
        Budget other = new Budget(2_000_000_000L);
        System.out.println("other.reserve=" + other.tryReserve(1_000_000_000L));
        System.out.println("other.used=" + other.used());
        Budget edge = new Budget(Long.MAX_VALUE);
        System.out.println("edge.first=" + edge.tryReserve(Long.MAX_VALUE - 3));
        System.out.println("edge.extra=" + edge.tryReserve(4));
        System.out.println("edge.used=" + edge.used());
        System.out.println("overflow=" + (Long.MAX_VALUE + 1));
    }
}

编译并运行:

bash 复制代码
javac LongFieldUpdaterDemo.java
java LongFieldUpdaterDemo

本例实际运行输出:

text 复制代码
successes=10
used=10000000000
extra=false
other.reserve=true
other.used=1000000000
edge.first=true
edge.extra=false
edge.used=9223372036854775804
overflow=-9223372036854775808

16次申请中有10次成功,累计值恰好100亿,再申请1字节会被拒绝。other使用同一个静态更新器,累计值却独立保存,因此它仍能成功预留10亿。

边界账户的上限设为Long.MAX_VALUE。第一次预留到Long.MAX_VALUE - 3,下一次申请4被拒绝,字段保持原值。最后一行单独展示普通long加法回绕,说明不能先相加再用结果判断容量。

线程池关闭前,我们已经通过每个Future的get等待所有任务完成,再读取最终累计值。读取的是这批申请完成后的结果,避免把执行中的中间值当成批次结论。

五、方法怎么选:累计、条件申请与精确加法

目标 适合的写法 返回值或行为
记录允许回绕的long累计值 getAndAdd 返回更新前的值
更新后立即读取累计值 addAndGet 返回更新后的值
超出业务额度就拒绝 读取+容量判断+CAS 成功才返回true
算术溢出需要抛出异常 Math.addExact配合更新函数 溢出抛ArithmeticException

原子操作保证并发更新的提交方式,不会自动把long变成无限精度整数。对金额、字节额度这样的有界累计,业务规则需要明确说明"超过上限怎样处理"。

若规则是算术溢出立即报错,可以写:

java 复制代码
long total = UPDATER.updateAndGet(target,
        current -> Math.addExact(current, delta));

Math.addExact 检测long相加溢出;它不会替你检查账户的业务额度。Math API

更新函数还可能因竞争重复计算,因此只做数值运算。发通知、写数据库、累加另一份计数等动作不要塞进这个函数。本例用明确的CAS循环返回成功与否,调用方能据此决定后续流程。

六、注意事项:额度预留与业务完成分开

预留成功只说明额度已经占住。

上传随后可能失败。实际服务需要规定何时释放预留、何时转为已完成用量,并用申请标识防止同一笔额度被重复释放。本文的Budget实现只提供单向累计,不包含退款或释放接口。

同样,used和数据库里的上传记录属于两份状态。单个long字段的CAS无法把两份状态一起提交;跨状态的一致性要在业务事务、状态机或可靠消息流程里设计。

选型时也可以先问数据放在哪里:独立计数对象用AtomicLong 直观;已有对象字段需要原子更新时,字段更新器可以承接这个字段;需要更细的内存访问模式时,再考虑VarHandle 。它们的取舍还要结合对象结构和维护成本,性能结论需要针对具体负载测量。原子类包说明

七、🧠 思维导图

#mermaid-svg-dJum4iIhwzL9yIWf{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-dJum4iIhwzL9yIWf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dJum4iIhwzL9yIWf .error-icon{fill:#552222;}#mermaid-svg-dJum4iIhwzL9yIWf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dJum4iIhwzL9yIWf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dJum4iIhwzL9yIWf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dJum4iIhwzL9yIWf .marker.cross{stroke:#333333;}#mermaid-svg-dJum4iIhwzL9yIWf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dJum4iIhwzL9yIWf p{margin:0;}#mermaid-svg-dJum4iIhwzL9yIWf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dJum4iIhwzL9yIWf .cluster-label text{fill:#333;}#mermaid-svg-dJum4iIhwzL9yIWf .cluster-label span{color:#333;}#mermaid-svg-dJum4iIhwzL9yIWf .cluster-label span p{background-color:transparent;}#mermaid-svg-dJum4iIhwzL9yIWf .label text,#mermaid-svg-dJum4iIhwzL9yIWf span{fill:#333;color:#333;}#mermaid-svg-dJum4iIhwzL9yIWf .node rect,#mermaid-svg-dJum4iIhwzL9yIWf .node circle,#mermaid-svg-dJum4iIhwzL9yIWf .node ellipse,#mermaid-svg-dJum4iIhwzL9yIWf .node polygon,#mermaid-svg-dJum4iIhwzL9yIWf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dJum4iIhwzL9yIWf .rough-node .label text,#mermaid-svg-dJum4iIhwzL9yIWf .node .label text,#mermaid-svg-dJum4iIhwzL9yIWf .image-shape .label,#mermaid-svg-dJum4iIhwzL9yIWf .icon-shape .label{text-anchor:middle;}#mermaid-svg-dJum4iIhwzL9yIWf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dJum4iIhwzL9yIWf .rough-node .label,#mermaid-svg-dJum4iIhwzL9yIWf .node .label,#mermaid-svg-dJum4iIhwzL9yIWf .image-shape .label,#mermaid-svg-dJum4iIhwzL9yIWf .icon-shape .label{text-align:center;}#mermaid-svg-dJum4iIhwzL9yIWf .node.clickable{cursor:pointer;}#mermaid-svg-dJum4iIhwzL9yIWf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dJum4iIhwzL9yIWf .arrowheadPath{fill:#333333;}#mermaid-svg-dJum4iIhwzL9yIWf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dJum4iIhwzL9yIWf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dJum4iIhwzL9yIWf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJum4iIhwzL9yIWf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dJum4iIhwzL9yIWf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJum4iIhwzL9yIWf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dJum4iIhwzL9yIWf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dJum4iIhwzL9yIWf .cluster text{fill:#333;}#mermaid-svg-dJum4iIhwzL9yIWf .cluster span{color:#333;}#mermaid-svg-dJum4iIhwzL9yIWf 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-dJum4iIhwzL9yIWf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dJum4iIhwzL9yIWf rect.text{fill:none;stroke-width:0;}#mermaid-svg-dJum4iIhwzL9yIWf .icon-shape,#mermaid-svg-dJum4iIhwzL9yIWf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dJum4iIhwzL9yIWf .icon-shape p,#mermaid-svg-dJum4iIhwzL9yIWf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dJum4iIhwzL9yIWf .icon-shape .label rect,#mermaid-svg-dJum4iIhwzL9yIWf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dJum4iIhwzL9yIWf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dJum4iIhwzL9yIWf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dJum4iIhwzL9yIWf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AtomicLongFieldUpdater
对象结构
共享更新器与独立状态
volatile long字段
额度预留
先比较剩余额度
CAS成功才生效
失败后重读重算
落地规则
维持非负有界状态
预留与完成分开
多状态设计整体流程

八、总结

总结要点

AtomicLongFieldUpdater把原子操作作用于对象的volatile long字段。更新器可以共享,累计状态仍由每个对象独立保存。

有上限的累计先计算剩余额度,再用CAS提交新值。竞争失败就重读重算,这样容量判断才能跟上真实状态,long相加也有明确的安全范围。

额度预留成功以后,还要继续管理业务执行与失败释放。单字段原子性解决一个状态的并发更新,完整业务流程负责其余状态之间的衔接。

下一篇继续介绍AtomicReferenceFieldUpdater,用对象字段发布一份完整的不可变状态。

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

相关推荐
WeiXin_DZbishe1 小时前
基于springboot城市公交实时运营与调度系统-计算机毕设 附源码【课程设计】66173
java·vue.js·spring boot·后端·python·php·课程设计
泡茶喝茶写代码1 小时前
A股量化数据工程:从 REST 接口到策略信号(第 18 篇):放量缩量异动扫描
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
wewowilling1 小时前
认识微服务
java·运维·微服务·springcloud
代码什么用2 小时前
Java Web 7-10
java·servlet
蜗牛互联网3 小时前
GPT-6 与 Intelligent UI:从回答到可审计操作的产品迁移
java·人工智能·后端
SL_staff4 小时前
运维救火实录:钉钉考勤API变更引发工资计算异常,我们用JVS-Logic可视化逻辑引擎实现分钟级热修复
java·mysql·全栈
SunnyDays10114 小时前
Java 查找并高亮 Word 文字:精确查找及正则表达式模糊查找
java·查找文字·word 文档·高亮文字·正则匹配查找·模糊查找
geats人山人海4 小时前
java 4继承
java·开发语言