i++不是原子操作,因其字节码必然拆分为iload(读取)、iinc或iadd(自增)、istore或putfield(写回)三步,中间无同步保障,多线程下存在竞态条件。

看 iload、iinc、istore 三步分离就能确认非原子性
Java 中 i++ 在字节码层面必然拆成至少三步:读取当前值 → 加 1 → 写回变量。这不是 JVM 的“优化”或“例外”,而是规范定义的行为。用 javap -c 反编译后,你能清晰看到这三类指令的独立存在,中间没有任何屏障或锁定语义包裹。
常见错误现象是以为加了 volatile 就能保证 i++ 原子——但 volatile 只影响读写可见性,不合并操作步骤。哪怕 getfield 和 putfield 都带 volatile 语义,中间的 iadd 或 iinc 仍发生在寄存器/操作数栈,不受其保护。
-
iload_1(或getfield):把变量值压入操作数栈,此时值已脱离内存上下文 -
iinc 1 by 1:直接对局部变量表第 1 个槽位做 +1(JVM 专为自增设计的快捷指令,但仍属独立步骤) -
istore_1(或putfield):把新值存回变量,和前面的读取不是同一个原子窗口
为什么 iinc 指令不能替代原子性保障
iinc 看似“一步到位”,但它只作用于局部变量表,不涉及字段读写同步逻辑。当目标是实例字段(如 this.num++)时,JVM 必须用 getfield + iadd + putfield 三指令组合,根本绕不开分离读-改-写。
使用场景上:iinc 仅适用于局部变量;而多线程竞争几乎总发生在对象字段或静态字段上——这时你看到的永远是 getfield、iadd、putfield 明确分立的三行字节码。
性能影响很小,但语义风险极大:每个指令都可能被线程调度打断。比如线程 A 执行完 getfield 后被挂起,线程 B 完成全部三步并写回,A 恢复后仍用旧值 iadd,最终结果丢失一次更新。
对比 ++i 和 i++ 的字节码差异不改变原子性本质
两者在原子性上完全等价——都非原子。区别只在返回值:前者先 iadd 再读,后者先读再 iadd。但无论哪种,读、加、写仍是三个可分割的动作。
例如 i = i++ 输出为 0,正是因为字节码中 iload_1(读旧值)→ iinc(改局部变量)→ istore_1(把旧值写回去),加的那 1 被覆盖了。这个现象恰恰反向验证了三步不可合并。
容易踩的坑:误以为“先加后赋值”就更安全。实际上,++i 的字节码里照样有独立的 iinc 或 iadd 步骤,一样会被并发打断。
别依赖汇编或 CPU 缓存行为推导 Java 原子性
JVM 规范只要求字节码语义正确,不承诺底层如何映射到硬件指令。不同 JVM 实现(HotSpot、OpenJ9)、不同 CPU 架构(x86、ARM)、甚至同一 JVM 的不同优化等级(C1/C2 编译),都可能导致实际生成的机器码完全不同。
真正可靠的依据只有两点:一是 javap -c 看到的字节码结构,二是 Java 语言规范(JLS §15.14.2, §15.15.1)明确定义 i++ 和 ++i 为“读-修改-写”复合操作,且未声明其原子性。
最常被忽略的一点:即使某次测试中 i++ 看似没出错,也不代表它线程安全——那是运气,不是保证。只要字节码里存在分离的读、算、写,就存在竞态窗口。











