如何在 Java 中使用 AtomicInteger 实现无锁的线程安全计数
如何在 Ja va 中使用 AtomicInteger 实现无锁的线程安全计数

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
先来看一个核心的技术论断:AtomicInteger的incrementAndGet通常比synchronized快,因为它基于CPU的CAS指令,避免了阻塞和上下文切换的开销。但事情总有另一面:在高争用场景下,它可能因频繁重试反而效率更低,并且它只适用于简单的原子操作。
AtomicInteger 的 incrementAndGet 为什么比 synchronized 快
关键在于底层机制。incrementAndGet直接调用了CPU的CAS(比较并交换)指令,这是一种非阻塞的乐观策略。线程不会进入阻塞队列,自然也就绕开了上下文切换和锁竞争带来的性能损耗。相比之下,synchronized在高并发压力下,可能会经历偏向锁、轻量级锁到重量级锁的升级过程。一旦膨胀为系统级的互斥锁,性能就会出现断崖式下跌。
不过,这里必须划个重点:CAS并非万能钥匙。在极端的高争用场景下——想象一下上千个线程反复争夺同一个AtomicInteger——CAS操作失败和重试的次数会急剧增加,CPU消耗可能不降反升,这时候它的表现甚至可能不如一把简单的锁。
- 适用场景:计数器、序列号生成、统计指标(如请求量、错误数)这类典型的“读多写少”或“写操作本身很简单”的场景。
- 不适用场景:需要原子性地执行多个变量联动更新的复杂操作(例如“从余额减100的同时记录一条流水”)。这种时候,就该考虑
Lock或事务机制了。 - 一个小提醒:
incrementAndGet()返回的是增加后的新值,而getAndIncrement()返回的是增加前的旧值。在条件判断等逻辑中,可别用反了。
compareAndSet 是唯一能做条件更新的原子操作
如果想实现“仅当当前值为某个特定值时才进行更新”,那么compareAndSet是唯一正确的选择。千万别试图先get()再set()——这两个操作之间的间隙就是一个竞态窗口,根本不是原子的。
来看一个典型的错误写法:
int cur = counter.get();
if (cur == 5) {
counter.set(6); // ❌ 危险!执行get()后,cur的值可能已经被其他线程修改了
}
正确的做法应该是这样:
int expected = 5; boolean updated = counter.compareAndSet(expected, expected + 1); // updated为true表示更新成功;为false则表示在此期间值已被改动,需要决定重试或放弃
compareAndSet是典型的乐观锁策略:假设冲突很少发生,失败了就重试。它适合低到中等争用的场景。- 如果业务逻辑本身就很复杂,或者重试的代价很高,那么硬套CAS可能得不偿失,不如直接使用
ReentrantLock。 - 注意参数顺序:
compareAndSet(expectedValue, newValue),千万别把期望值和新值的位置写反了。
AtomicInteger 不能替代 long 类型的原子运算
这是一个容易踩坑的地方。AtomicInteger包装的是int类型(32位),其最大值是Integer.MAX_VALUE(2147483647)。一旦计数超过这个值,incrementAndGet()不会抛出异常,而是会发生静默的整数溢出,从最大值翻转到最小值(-2147483648)。
如果你的计数器有超过21亿的可能(比如全局日志行数、海量消息处理量),就必须换用其他方案:
AtomicLong:支持64位长整型,上限约9×10¹⁸,足以应对绝大多数场景。LongAdder:在超高并发的累加场景下,它的性能通常比AtomicLong更优。其内部采用了分段累加的策略来减少CAS争用。但需要注意的是,它不支持compareAndSet操作。- 切记,不要自己用
synchronized包裹一个long变量来模拟原子性,这等于放弃了无锁编程的全部优势。
get() 和 lazySet() 的内存语义差异常被忽略
get()是一个volatile读,它能保证线程总是能读到最新的值。而lazySet()(可以看作是set()的一个弱化版本)只保证写入操作本身不会被指令重排序,但并不保证这个新值能立即被后续的读操作看到——JVM可能会延迟将其刷新回主内存。
这意味着:
- 在写密集、且读写操作没有紧密耦合的场景下(例如信号量清零、设置一个状态标记),用
lazySet(value)替代set(value)可以略微提升写性能。 - 但是,它绝对不能用在对写后立刻读有强依赖的逻辑中。例如:
counter.lazySet(0); assert counter.get() == 0;这个断言是有可能失败的。 - 对于大多数业务代码而言,
lazySet可能根本用不上。除非你在明确的性能压测中发现set()成为了瓶颈,并且能够接受最终一致性的语义。
说到底,无锁编程并非没有成本,它只是将同步的成本从线程的阻塞和切换,转移到了CPU的重试循环和内存屏障上。因此,最关键的一步,是清醒地评估你的计数场景是否真的需要无锁方案。很多号称“高并发”的系统,其核心计数器的更新频率可能每秒只有几百次,在这种量级下,使用synchronized不仅完全够用,而且代码更直观,更不容易出错。
相关攻略
Ja va 中 String getBytes() 返回不同结果的原因解析 String getBytes() 每次调用返回的是新创建的 byte[] 实例,其 toString() 默认输出为内存地址标识(如 [B@1b6d3586),因此看似“不同”;但数组内容完全一致,差异仅源于对象引用不同。
如何在 Ja va 中利用 WeakReference 防止由于缓存对象导致的内存溢出 先说一个核心结论:WeakReference 不能直接用于常规缓存,它只适合“可丢弃”的临时引用场景。 很多开发者误以为它能自动管理内存,结果掉进了坑里。 为什么 WeakReference 不适合做通用缓存 道
如何在 Ja va 中利用 Collectors collectingAndThen() 在收集完成后将结果转为不可变 collectingAndThen() 的核心作用不是“变不可变”,而是“后处理” 首先得澄清一个常见的误解:Collectors collectingAndThen() 本身并不
Ja va SSL调试日志中如何唯一标识多TLS连接? Ja va SSL调试日志本身不直接标记TLS连接ID,但可通过线程ID(第3字段)与线程名(第4字段)组合,在单次握手生命周期内准确定位归属;需注意线程复用场景下该组合仅反映处理线程而非连接本身。 排查多TLS连接问题时,面对满屏的SSL调试
如何通过 Unsafe 类操作 CPU 的 Memory Barrier 实现在 Ja va 层的无锁屏障设计 先说一个核心事实:Ja va 层无法直接通过 Unsafe 发出 CPU 级 Memory Barrier 指令。 我们常用的 loadFence()、storeFence()、fullF
热门专题
热门推荐
Origin Code发布VORTEX系列专用分体式水冷冷头模块 2026年4月7日,知名内存模组品牌Origin Code正式发布了专为VORTEX系列内存打造的分体式水冷冷头模块,官方售价为899元。这款产品的推出,为追求极致散热性能、低温和系统视觉一体化的高端DIY玩家及超频爱好者,提供了一个
荣耀WIN游戏本定档4月23日:性能释放突破250瓦,电竞体验全面升级 2026年4月7日,荣耀正式揭晓了全新WIN游戏本的发布日期:4月23日。这款备受瞩目的产品其实早已不是秘密,早在去年12月,荣耀PC产品负责人就已经在公开渠道透露了新品的进展,并确认了一个关键身份——它将成为《三角洲行动》职业
内存供应趋紧,苹果部分Mac交付周期显著延长 进入2026年第二季度,全球半导体产能的重新分配仍在持续。一个不容忽视的趋势是,人工智能应用的爆发式增长,正持续推高对高性能内存芯片的需求,导致DRAM市场供应整体趋紧。自去年下半年开始的这轮价格上涨,让终端设备制造商普遍感受到了成本压力,即便是供应链管
荣威全新i6上市:7 49万起售,搭载8155芯片与国潮 2026年4月30日,荣威品牌旗下的全新一代紧凑型轿车i6正式推向市场。新车一口气带来了三款配置,分别命名为长久版、豪久版与臻久版,官方给出的指导价区间定在7 49万元到8 49万元。不过,眼下正值上市初期,官方还推出了限时抢订政策,实际支付
暗黑破坏神4:憎恨之王上线后,术士职业迅速跻身当前版本最具统治力的职业行列 其核心能力涵盖恶魔召唤、地狱火攻击与神秘印记体系,其中一种以“召唤即献祭”为运转逻辑的召唤流派正展现出显著优势。 这次资料片带来的技能系统重构,可以说是一次彻底的革新:所有被动技能被移除,每个主动技能都扩展成了拥有多节点分支





