i++非原子操作的根源在于字节码四步分离:getstatic/getfield→iconst_1→iadd→putstatic/putfield,任意两步间可被中断;volatile仅保可见性不保原子性;唯atomicinteger.incrementandget()等明确api才真正原子。

javap -c 看到的四步就是竞态根源
直接反编译含 i++ 的方法,javap -c 输出里必然出现这四个关键指令:getstatic(或 getfield)、iconst_1、iadd、putstatic(或 putfield)。它们不是原子打包执行的,而是分步压栈、计算、回写——任意两步之间都可能被其他线程插入。比如线程 A 执行完 getstatic 读到值 5,还没走到 iadd,线程 B 就已完整跑完四步把值改成了 6;等 A 继续加 1 再写回,结果仍是 6,而非预期的 7。
static i++ 和 instance i++ 的字节码差异不影响原子性结论
静态变量用 getstatic/putstatic,实例变量用 getfield/putfield,但无论哪种,都逃不开“读→算→写”三阶段。区别只在内存寻址路径(类元数据区 vs 对象头),不改变操作可分割的本质。哪怕你把 i 声明为 volatile int i,getfield 和 putfield 能保证可见性与禁止重排序,但中间的 iadd 仍在线程私有栈上独立执行,无法阻止两个线程同时读到同一个旧值。
别信“单行代码=安全”,真正能当原子用的只有明确标注的 API
-
i = 1是原子的(基本类型纯赋值) -
obj = new Foo()是原子的(引用赋值) -
AtomicInteger.incrementAndGet()是原子的(底层 lock xadd 或 cmpxchg) -
i++、i += 2、list.add(x)、map.get(k) == null ? map.put(k, v) : map.get(k)全都不行——只要涉及“先读再算再写”,就踩进非原子雷区
验证竞态最简单的方式:写个 10 线程各跑 1000 次的测试
不用复杂框架,几行代码就能复现问题:
static int i = 0;
public static void main(String[] args) throws InterruptedException {
List<thread> threads = new ArrayList();
for (int t = 0; t {
for (int j = 0; j { try { t.join(); } catch (Exception e) {} });
System.out.println(i); // 多数情况下远小于 10000
}</thread>
这个结果不稳定本身,就是非原子性的铁证。想靠 sleep、yield 或 volatile 来“修复”它,只会掩盖问题,不会消除隐患。
真正难的不是看懂字节码,而是习惯性质疑每一处“读-改-写”组合——哪怕它只占一行、看起来毫无威胁。











