volatile不能保证原子性,因其仅确保可见性和禁止重排序,而i++等复合操作涉及读-改-写三步,仍可能被多线程交错执行。

volatile 为什么不能保证原子性
声明为 volatile 的变量,读写操作会强制刷新主内存,但像 i++ 这种复合操作(读–改–写)仍可能被多个线程交错执行。JVM 不会对它加锁或生成原子指令,底层仍是三条独立字节码:getfield、iadd、putfield。
常见错误现象:多个线程对 volatile int counter 执行 counter++,最终结果远小于预期。
- 适用场景:状态标志位(如
running)、单次写入多次读取的配置项 - 不适用场景:计数器、累加器、依赖当前值计算新值的逻辑
- 替代方案:需要原子性时,用
AtomicInteger或同步块
volatile 如何禁止指令重排序
JVM 和 CPU 都可能对指令重排以优化性能,但 volatile 写操作会插入「StoreStore」屏障,读操作插入「LoadLoad」和「LoadStore」屏障,确保其前后的内存访问顺序不被跨过。
典型问题:双重检查锁定(DCL)中未加 volatile,可能导致构造函数未执行完,其他线程就看到非 null 的实例引用。
立即学习“Java免费学习笔记(深入)”;
- 关键点:仅对
volatile字段本身及其前后访问有约束,不扩散到其他普通字段 - 参数差异:无需额外配置,是语言级语义,所有 JVM 实现必须遵守
- 注意:重排序禁止 ≠ 执行顺序保证,它不控制代码执行先后,只约束内存可见边界
volatile 在 long/double 上的特殊行为
Java 规范允许 32 位 JVM 对 long 和 double 的读写拆成两次 32 位操作(即“非原子性”),导致可能读到“半个值”。加 volatile 后,强制该读写作为单个原子操作处理。
常见错误现象:在 32 位 JDK 上,未声明为 volatile 的 long timestamp 被并发读写,偶尔出现高位或低位错乱的数值。
- 使用场景:跨线程共享时间戳、序列号等 64 位整型字段
- 兼容性影响:64 位 JVM 通常默认原子,但规范不保证,仍建议显式加
volatile - 不要依赖平台实现细节,一律按规范来
volatile 与 synchronized 的核心区别
volatile 只解决可见性和重排序,不提供互斥;synchronized 同时提供原子性、可见性、有序性,但有锁开销。
容易踩的坑:以为加了 volatile 就不用同步了,结果发现临界区逻辑仍出错;或者过度使用 synchronized 去保护纯状态标志,拖慢高频读场景。
- 性能影响:volatile 读几乎无开销,写略高于普通变量;synchronized 在无竞争时已优化,但竞争激烈时明显变慢
- 选择依据:仅需通知变化 → volatile;需保护一段代码块或防止多线程同时进入 → synchronized 或 Lock
- 组合使用:volatile 标志 + synchronized 临界区 是常见且合理的模式
真正难的是判断“这个变量的变化是否只靠可见性就够了”。很多看似简单的标志位,一旦和后续逻辑耦合(比如“设为 false 后立刻中断某循环”,而循环里又依赖另一个非 volatile 变量),就会掉进重排序或缓存不一致的坑里。









