i++不具备原子性,因其在JVM中分为read、add、write三步,多线程下易导致结果丢失;应依场景选用synchronized、AtomicInteger或LongAdder等正确工具。

原子性不是“看起来像一条语句”
很多人看到 i++ 就以为它是一条不可拆分的指令,其实它在 JVM 层面对应三个独立步骤:read(读取当前值)、add(加 1)、write(写回主内存)。这三个动作之间可能被其他线程插入执行,导致最终结果丢失。比如两个线程同时对初始值为 0 的 i 执行 i++,最终结果可能是 1 而不是 2。
Java 中基础类型自增的并发风险点
直接用 int、long 等基本类型做计数器,在多线程下几乎必然出错。常见错误现象包括:
- 计数值长期卡在某个小数字上不增长(高并发低冲突时重试失败堆积)
- 计数值比预期少很多(多个线程读到同一旧值,各自加 1 后写回,覆盖彼此)
- 使用
volatile int i仍无法修复问题(volatile只保证可见性和禁止重排,不保证i++这类复合操作的原子性)
怎么安全地做自增:选对工具再动手
不要自己手写锁或 CAS 循环,除非你明确知道为什么需要它。优先按这个顺序选方案:
- 低频更新、逻辑复杂 → 用
synchronized块包裹自增逻辑,简单可靠 - 高频计数、只做加减 → 用
AtomicInteger.incrementAndGet()或AtomicLong.addAndGet(1) - 极高并发、吞吐敏感 → 考虑
LongAdder(它把热点分散到多个 cell,避免 CAS 激烈竞争) - 绝对不要用
new Integer(i).intValue()或包装类自动拆箱来“绕过”问题 —— 它们没用,且更慢
Go 里 atomic.AddInt32 和 Java 的 AtomicInteger 不是等价替换
Go 的 atomic.AddInt32(&x, 1) 是底层硬件指令直通,无函数调用开销;Java 的 incrementAndGet() 是 JVM 对 Unsafe.compareAndSwapInt 的封装,有少量方法调用和边界检查成本。但更重要的是语义差异:
- Go 的
atomic包所有操作都默认带SequentialConsistency内存序,Java 的AtomicInteger也同理,这点一致 - 但 Go 中
atomic.LoadInt32和atomic.StoreInt32是显式配对,Java 中对应的是get()和set(),而volatile字段的读写行为与它们近似但不完全等价(例如不能用于final字段初始化后修改) - 误把 Java 的
AtomicInteger当成“万能 volatile 替代品”是常见误区 —— 它解决的是原子性,不是所有可见性场景都适用(比如作为状态标志配合 while 循环时,volatile boolean更轻量)
真正容易被忽略的,是自增操作背后隐含的“读-改-写”三步耦合。哪怕你用了 AtomicInteger,如果把它嵌套在更大的非原子逻辑里(比如先 get() 判断再 incrementAndGet()),整个逻辑块依然不是原子的 —— 这时候得靠锁或更高层的协调机制。










