原子性取决于“读、改、写”是否被拆分为多条字节码指令,如i++对应getfield→iadd→putfield三步,中间无内存屏障,故非原子;iinc仅适用于局部变量且不保证跨线程原子性;volatile不能合并操作步骤;真正原子需CAS或锁机制。
直接看字节码就能判断一个操作是否具备原子性——关键不是“有没有锁”,而是“读、改、写”三步是否被拆开执行。java 中像 i++、count++ 这类复合操作,天然不具备原子性,根源就在字节码指令的分离上。
从 getfield → iadd → putfield 看非原子性的物理痕迹
对实例字段执行 this.count++,javap 反编译后必然出现三行独立指令:
- getfield:从主内存加载字段值到操作数栈,此时线程已脱离最新内存状态
- iadd:在操作数栈完成加法运算,该步骤不触碰内存,纯寄存器/栈内计算
- putfield:把结果写回主内存,但写入的是基于旧快照算出的值
这三步之间没有内存屏障,也没有指令级锁定。JVM 规范强制要求这种分步实现,不是优化行为,而是语义定义。哪怕 CPU 缓存一致、指令不重排,只要线程调度发生在 getfield 和 putfield 之间,就可能被另一个线程覆盖结果。
局部变量用 iinc 也不等于原子
有人注意到 i++ 在局部变量场景下会生成 iinc 1 by 1 单条指令,误以为“一步到位=原子”。但要注意:
-
iinc只作用于局部变量表,不涉及主内存同步逻辑 - 多线程竞争几乎总发生在对象字段或静态字段上,这时根本不会用
iinc,而是明确拆成getfield + iadd + putfield - 即使局部变量真被多线程共享(如通过逃逸分析失效),
iinc仍无法保证跨线程可见性与执行不可中断性
volatile 修饰字段也不能拯救 i++
volatile 只保障单次读或单次写的可见性与禁止重排序,它不合并操作步骤:
-
volatile int count的count++仍会生成getfield(带 volatile 语义)、iadd、putfield(带 volatile 语义)三步 - 中间的
iadd发生在操作数栈,不受 volatile 约束;两个线程仍可同时读到相同旧值,各自加 1 后写回,最终只加了一次 - volatile 解决的是“读到过期值”的问题,不是“两次写互相覆盖”的问题
真正能保证原子性的底层路径
要让数值更新具备原子性,必须绕过“读-改-写”流程,走硬件支持的单指令路径:
-
CAS 指令:如
AtomicInteger.incrementAndGet()底层调用Unsafe.compareAndSwapInt(),对应 CPU 的cmpxchg指令,一次性完成“比较并交换”,失败则重试 -
锁机制:synchronized 或 ReentrantLock 通过管程进入/退出建立 happens-before 关系,把整个
count++块变成临界区,从逻辑上串行化执行 - long/double 的特例:JMM 明确规定 64 位变量在未声明为 volatile 时,读写可能被拆成两个 32 位操作(非原子),而声明为 volatile 后,读写才作为单个原子操作执行

















