volatile 保证 long/double 的单次读或写原子性,通过 JVM 底层指令或同步机制实现;但不保证复合操作原子性,现代 JDK 中该作用多为历史兼容,核心价值在于可见性和禁止重排序。

volatile 对 long/double 的原子性作用,仅限于单次读或写
Java 语言规范(JLS §17.7)明确指出:对于非 volatile 的 long 和 double 变量,JVM 允许将 64 位的读写操作拆分为两个 32 位操作。这在 32 位 JVM 上尤其真实——比如写入一个 long 值 0x1122334455667788,可能先写高 32 位 0x11223344,再写低 32 位 0x55667788;若此时另一线程恰好读取,就可能拿到“高 32 位新 + 低 32 位旧”的撕裂值(torn value),如 0x1122334400000000。
而一旦用 volatile 修饰,JVM 必须保证对该变量的每次读或每次写,都作为不可分割的 64 位整体执行。这不是靠 Java 层逻辑实现的,而是由 JVM 在底层通过以下方式保障:
- 在 32 位 HotSpot 中,插入内存屏障并配合锁总线或 CAS 指令,确保两次 32 位操作不被中断或交错
- 在 64 位 HotSpot(Java 5+)中,直接使用原生 64 位寄存器和指令(如
mov rax, [mem]),天然原子;volatile 此时主要起语义强化作用,让行为可预测、可移植
它不保证复合操作,也不解决竞态逻辑
volatile 仅对“一次读”或“一次写”提供原子性边界。像 value++、value += 10 或 if (value == 0) value = 1; 这类操作,无论变量是否为 volatile long,都不是原子的——它们包含读、算、写多个步骤,中间完全可能被其他线程抢占。
例如:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
volatile long counter = 0;- 两个线程同时执行
counter++ - 两者都可能读到 0 → 都算出 1 → 都写回 1 → 最终结果是 1,而非预期的 2
这种更新丢失问题,volatile 无能为力。必须改用 AtomicLong、synchronized 或 ReentrantLock。
现代开发中,你大概率不需要靠它保 long/double 原子性
自 Java 5 起,主流 JDK(OpenJDK 8u292+、11、17、21)在 64 位 JVM 上默认以原子方式处理 long/double 读写。即使在较新的 32 位 OpenJDK(如某些嵌入式裁剪版)中,JVM 实现也普遍通过底层同步机制规避了撕裂风险。
也就是说:
- 如果你用的是近十年发布的标准 JDK,普通 long/double 读写已是事实原子
- volatile 的真实价值,在于可见性和禁止重排序,比如:
volatile boolean shutdownRequested = false; - 只有在明确运行于老旧 32 位 JVM(如某些工业控制器、POS 终端)且变量需高频跨线程读写时,volatile 才承担“兜底原子性”的角色
一句话总结
volatile 让 long/double 的单次读或单次写成为原子操作,是 JVM 规范强制要求的行为,靠底层指令或同步机制实现;但它不延伸至任何复合操作,也不替代真正的线程安全方案。在绝大多数现代 Java 环境中,它对 long/double 的原子性保障已成“历史兼容特性”,而其可见性与有序性价值始终关键。

















