游乐游手机版
首页/编程语言/文章详情

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

时间:2026-04-30 11:14
如何在 Ja va 中使用 AtomicInteger 实现无锁的线程安全计数 先来看一个核心的技术论断:AtomicInteger的incrementAndGet通常比synchronized快,因为它基于CPU的CAS指令,避免了阻塞和上下文切换的开销。但事情总有另一面:在高争用场景下,它可能因

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

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

先来看一个核心的技术论断:AtomicIntegerincrementAndGet通常比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不仅完全够用,而且代码更直观,更不容易出错。

来源:https://www.php.cn/faq/2393324.html
上一篇如何在 Java 中通过 Constructor.newInstance() 动态创建类的实例对象 下一篇怎么在 Java 中声明并初始化基础数据类型(int, double, boolean)
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
PyTorch中使用多维索引张量对高维张量批量索引的正确方法
编程语言 · 2026-07-03

PyTorch中使用多维索引张量对高维张量批量索引的正确方法

本文深入讲解如何在 PyTorch 中利用形状为 [b, k] 的索引张量 B,对形状为 [b, m, n] 的高维张量 A 执行高效批量索引,最终得到 [b, k, n] 的输出。核心思路在于合理扩展索引维度并配合 torch gather 实现精准的逐行抽取。 很多人处理高维张量的批量索引时都会

Go中...操作符解包切片传递可变参数函数
编程语言 · 2026-07-03

Go中...操作符解包切片传递可变参数函数

在 Go 语言中,` ` 运算符放在切片变量后面(如 `slice `)的作用是将该切片“展开”为多个独立参数,专门用于调用那些接受可变参数(` T`)的函数,例如 `append` 或 `fmt Println`。这是一种类型安全的语法糖,并非省略号或通配符,能够帮助开发者更简洁地处理

macOS与WSL2下PHP多版本切换失效问题排查与修复指南
编程语言 · 2026-07-03

macOS与WSL2下PHP多版本切换失效问题排查与修复指南

本文深入分析在 macOS 或 WSL2(Ubuntu)开发环境中,通过 Homebrew 管理 PHP 多版本时,php -v 始终显示旧版本(如 php@5 6)的深层原因,并给出系统性解决方案,覆盖 PATH 冲突、符号链接逻辑、Shell 初始化配置、系统残留配置等关键环节。 遇到这种情况的

PHP JSON解析深层嵌套对象属性访问失败的解决方法
编程语言 · 2026-07-03

PHP JSON解析深层嵌套对象属性访问失败的解决方法

使用 json_decode() 解析 API 返回的 JSON 数据时,经常遇到某个子属性无法正常获取,始终返回 NULL —— 这是许多 PHP 开发者都曾碰到过的棘手问题。通常并非数据丢失,而是对象嵌套层级比预期更深,导致访问路径不正确。 举例来说,你看到返回的 JSON 里有一个 appea

nnU-Net v2预处理卡死问题的成因分析与实用解决指南
编程语言 · 2026-07-03

nnU-Net v2预处理卡死问题的成因分析与实用解决指南

> 使用 nnUNetv2_plan_and_preprocess 处理大规模数据集(例如 704 例样本)时,程序常因多进程加载导致死锁而停滞。核心原因在于默认并发数过高引发资源竞争或 I O 阻塞,适当降低并发数即可稳定完成全量预处理。 你在使用 `nnunetv2_plan_and_prepr