long/double的非原子性协定指JMM允许JVM将非volatile的64位变量读写拆为两次32位操作,导致多线程下可能读到“半更新”值;该协定与指令重排无关,但二者叠加会加剧撕裂读风险。

Java 中 long 和 double 的非原子性读写,并非源于 JVM 指令重排,而是由 Java 内存模型(JMM)明确规定的“非原子性协定”——即在 32 位平台(或未启用特定优化的 JVM)上,对 64 位变量的读/写可能被拆分为两个 32 位操作。指令重排是另一个独立问题,它可能加剧可见性风险,但不是该协定的根源。
什么是 long/double 的非原子性协定?
根据 Java 语言规范(JLS §17.7),对非 volatile 的 long 或 double 变量的单次读或写操作,不要求是原子的。这意味着:
- JVM 可将其拆成两次 32 位操作(高位 + 低位);
- 在多线程环境下,一个线程可能读到“半更新”的值(例如:高位是旧值、低位是新值);
- 该行为仅针对非
volatile且未被同步保护的变量; - 现代 64 位 JVM(如 HotSpot)在开启
-XX:+UseCompressedOops或默认配置下,通常对齐访问能保证原子性,但 JMM 仍不保证——程序不可依赖此实现细节。
指令重排如何与该协定交互?
指令重排本身不导致非原子性,但它会放大其危害:
- 编译器或 CPU 可能将对
long的写入与其他内存操作重排序,使“半写”状态更早对其他线程可见; - 若无
volatile或锁,重排 + 非原子写 → 其他线程不仅可能看到撕裂值,还可能在错误时机看到它; - 例如:
flag = true(普通布尔)和data = 0x1234567890ABCDEFL(普通 long)之间无 happens-before 关系,重排后,线程 B 可能在data尚未完全写完时就看到flag == true,进而读取到撕裂的data。
如何正确保障 long/double 的原子性与可见性?
唯一可移植、符合 JMM 的方式是显式同步:
立即学习“Java免费学习笔记(深入)”;
-
声明为
volatile:强制读写原子性 + 禁止重排 + 建立 happens-before; -
用
synchronized保护读写:利用管程的原子性和内存语义; -
使用
AtomicLong/AtomicLongFieldUpdater:底层通过volatile或 CAS 实现,提供更强操作(如 addAndGet); - 避免依赖“JVM 实现看起来原子”——这不属于规范保证,禁用压缩指针(
-XX:-UseCompressedOops)或切换 CPU 架构都可能打破它。
验证非原子性的典型场景(仅用于理解,勿用于生产)
可在老旧或刻意配置的 JVM 上观察(如 32 位 JVM + 非 volatile long):
- 线程 A 循环写入不同值(如
0x00000000FFFFFFFFL,0xFFFFFFFF00000000L); - 线程 B 循环读取并检查是否等于任一写入值;
- 若出现既不等于前者也不等于后者的结果(如
0x0000000000000000L或中间态),即为撕裂读; - 注意:现代 HotSpot 在默认 64 位模式下极少复现,不代表问题消失,只代表实现做了额外保证。


















