volatile不能保证i++的原子性,因其仅确保可见性和禁止重排序,而i++包含读—改—写三步,中间可能被其他线程打断。

volatile 不能保证 i++ 的原子性
直接说结论:volatile 只保证可见性和禁止指令重排序,不保证复合操作的原子性。i++ 看似简单,实则是“读—改—写”三步:先读取 i 的当前值,再加 1,最后写回内存。哪怕 i 是 volatile 修饰的,这三步之间仍可能被其他线程打断。
常见错误现象是:多线程下对 volatile int i = 0 执行 1000 次 i++,最终结果远小于 1000 —— 这不是偶发 bug,是必然发生。
-
i++在字节码层面至少对应三条指令:getstatic、iadd、putstatic -
volatile仅确保每次getstatic和putstatic都直通主存,但不锁住中间过程 - 两个线程同时读到
i == 5,各自加 1 后都写回6,一次累加就丢了
为什么 AtomicInteger.incrementAndGet() 能行
它用的是 CAS(Compare-And-Swap)+ 自旋,把“读—改—写”封装成一个不可分割的硬件级操作。JVM 会将其编译为类似 lock xadd 的 CPU 指令(x86 平台),由处理器保证执行期间不会被抢占。
使用场景明确:需要在多线程中安全修改单个整型计数器,且不希望引入重量级锁(如 synchronized)时,AtomicInteger 是首选。
立即学习“Java免费学习笔记(深入)”;
- 不是所有方法都原子:
AtomicInteger.get()和set()是,但getAndIncrement()和incrementAndGet()才真正对应i++和++i - CAS 在高竞争下会自旋重试,极端情况下可能比
synchronized更耗 CPU - 注意其
lazySet()方法:写操作不立即刷主存,适合做“只写不读”的标记位(如关闭标志)
synchronized 做累加也完全可行,但要注意粒度
很多人以为 AtomicInteger 是唯一解,其实用 synchronized 同样能正确实现并发累加,关键是锁的范围要小、对象要稳。
容易踩的坑是锁了不该锁的东西,比如每次新建一个 Object 当锁,或者锁了 this 却在不同实例上调用 —— 根本没起到互斥作用。
- 推荐用私有 final 锁对象:
private final Object lock = new Object();,避免外部干扰 - 不要锁字符串字面量或
Class对象,除非你清楚类加载和字符串常量池的共享逻辑 - 如果累加只是某个方法内的局部行为,且该方法本身已同步,就不必额外加锁
别忽略 long/double 的特殊性:volatile 也不保原子读写
Java 规范允许 JVM 将 64 位的 long 或 double 的读写拆成两个 32 位操作(尤其在 32 位 JVM 上)。这意味着即使加了 volatile,也可能出现“半个值被更新、另半个还是旧值”的撕裂现象(torn write)。
所以,只要涉及多线程读写 long 或 double,无论是否 volatile,都得用 AtomicLong / AtomicLongFieldUpdater,或者用 synchronized 包裹读写逻辑。
-
volatile long timestamp在某些老 JVM 上,可能被读出一个高位来自旧值、低位来自新值的“拼接数” -
AtomicLong内部同样依赖 CAS,且对 64 位操作做了平台适配,是唯一可靠选择 - 现代 64 位 JVM 通常默认原子处理
long/double,但规范不保证,生产环境不能赌
真正难的不是选 AtomicInteger 还是 synchronized,而是想清楚:这个累加是否真的需要跨线程可见?是否和其他状态变更存在耦合?一旦涉及多个变量协同更新,volatile 和原子类都无能为力,只能靠锁或更高级的并发结构。









