volatile long/double在32位jvm上读写可能非原子,导致读到高低位拼接的错误值;64位jvm默认原子,但复合操作仍需atomiclong等保证;运行时应通过sun.arch.data.model确认位数。

volatile long/double 在32位JVM上不保证原子读写
Java语言规范明确指出:volatile 修饰的 long 和 double 变量,在32位虚拟机(或某些旧版JVM实现)上,**可能被拆分为两个32位操作执行**。这意味着一次 read 或 write 可能不是原子的——你可能读到“半个新值、半个旧值”的混合结果。
常见错误现象:long 计数器在多线程下出现非预期的跳变或回退;double 时间戳偶尔变成极大负数或 NaN;日志里反复看到 0x00000000FFFFFFFF 这类明显拼接痕迹的值。
- 只在32位JVM或启用了
-XX:+UseCompressedOops但未启用-XX:+UseSplitStacks(已废弃)等特殊配置下才实际暴露,64位JVM默认用单条指令读写64位数据,天然原子 -
volatile能保证可见性和禁止重排序,但**不扩展原子性边界**——它对long/double的“原子性承诺”仅限于JVM实现能用单指令完成时 - 不用
volatile改用AtomicLong/AtomicDouble是最稳妥的替代方案,它们内部通过Unsafe.compareAndSwapLong等机制强制保证64位操作整体性
为什么 AtomicLong 比 volatile long 更安全
AtomicLong 底层不依赖JVM对 volatile long 的实现细节,而是直接调用 Unsafe 提供的原子CAS指令(如 compareAndSwapLong),这些指令在x86/x64平台对应 cmpxchg8b 或 cmpxchg16b,硬件级保障64位读-改-写原子性。
使用场景:计数器、序列号生成、带条件更新的状态字段(比如“仅当当前值小于阈值时递增”)。
立即学习“Java免费学习笔记(深入)”;
-
AtomicLong.get()和volatile long读性能接近,但set()略慢(因需内存屏障),而incrementAndGet()等复合操作则完全避免了拆分风险 - 注意
AtomicLong不提供volatile那种“无锁可见性广播”语义——它的每次get()都是新鲜值,但开销略高;若只是单纯发布一个只写一次的配置值,volatile仍更轻量 - 别误以为
AtomicLong构造函数参数能绕过初始化问题:静态字段用new AtomicLong(0)和new AtomicLong(Long.MIN_VALUE)行为一致,重点在后续操作是否原子
如何快速判断你的JVM是否存在拆分风险
运行时检测比查文档更可靠。关键看JVM是否以64位模式启动,并确认目标CPU支持原生64位加载存储。
- 执行
java -version,输出含64-Bit字样即大概率安全;若显示32-Bit或无明确标识,需进一步验证 - 代码中打印
System.getProperty("sun.arch.data.model")——返回"64"才表示JVM以64位模式运行(即使底层OS是32位,某些JVM也能模拟) - 极端情况下(如嵌入式JVM或老版本Android ART),可写一个循环反复
volatile long写入再读取,检查是否出现中间态(如高位为0、低位非0),但该测试本身有竞态,仅作辅助
volatile long/double 的合理使用边界
它不是“错误”,而是有明确适用前提:你确定运行环境是64位JVM,且不需要对变量做复合操作(如先读再算再写)。
典型安全场景:状态标志(volatile long shutdownTime)、单次写入的配置项(volatile double timeoutMs)、配合 synchronized 使用的辅助字段(此时原子性由锁保证,volatile 仅负责可见性)。
- 只要涉及
++、+=、max(current, candidate)这类读-改-写逻辑,就必须换AtomicLong或加锁,volatile无法兜底 - 不要为了“看起来轻量”而坚持用
volatile long——现代JVM对AtomicLong的内联和屏障优化已经非常成熟,性能差距远小于逻辑出错的代价 - 如果项目要兼容 Android(尤其旧版本)、某些IoT JVM或自定义移植版,直接默认禁用
volatile long/double,统一走Atomic*类型
真正麻烦的从来不是写法,而是那个“应该没问题吧”的假设——它往往藏在压测没覆盖的长尾路径里,等上线后某个凌晨三点才浮现。










