volatile 关键字可以保证原子性吗?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官问这个问题,核心是想考察:

  1. volatile 关键字核心语义的准确理解:你是否清楚 volatile 能保证什么(可见性、有序性),不能保证什么(原子性)。这是基础。
  2. 对“原子性”概念的理解深度:面试官不仅仅想知道你是否能背出“不能”,更想知道你是否理解原子性在并发编程中的具体含义(例如,一个或多个操作不可分割的整体)。
  3. 对 JMM(Java内存模型)的实践认知:通过这个问题,可以引出你对共享变量多线程访问、工作内存与主内存交互等底层机制的理解。
  4. 解决实际问题的能力:当你指出 volatile 的不足后,面试官通常会跟进问:“那么如何保证原子性?”,以此考察你是否了解 synchronizedLockjava.util.concurrent.atomic 包下的原子类等正确方案。

核心答案

不能。 volatile 关键字无法保证原子性

它只能保证两大特性:

  • 可见性:对一个 volatile 变量的写操作,会立即刷新到主内存,并导致其他线程中该变量的缓存失效,从而保证读取到最新值。
  • 有序性(禁止指令重排序):通过插入内存屏障,防止 JVM 和处理器对 volatile 变量读写操作进行重排序。

典型的反例是 i++(自增)操作。即使 i 被声明为 volatilei++ 这个 “读取-修改-写入” 组合操作在多线程环境下依然不是原子的,仍会导致计数错误。

深度解析

原理/机制

为什么 volatile 管不了原子性?这需要从 JMM 和操作本身来分析。

  1. 原子性的定义:一个或多个操作作为一个不可分割的整体,要么全部执行成功,要么全部不执行,中间状态对外不可见。
  2. 复合操作的困境:像 i++flag = !flag 这类操作,在字节码和 CPU 指令层面通常由多个步骤组成(例如 i++ 包含:读取 i 的值、给 i 加 1、将新值写回 i)。volatile 能保证每一步的读或写是可见且有序的,但它无法将这三个步骤“捆绑”成一个原子操作。
  3. 问题发生场景:当线程 A 读取 volatile 变量 i=5 到工作内存后,在它进行加1和写回之前,线程 B 也可能读取到 i=5。随后两个线程各自加1并写回主内存,最终结果可能是 6,而不是正确的 7。这就是著名的“丢失更新”问题。

代码示例

public class VolatileAtomicityDemo {
    // 即便使用了 volatile,也无法保证 count++ 的原子性
    private volatile int count = 0;

    public void increment() {
        count++; // 这是一个非原子操作
    }

    public static void main(String[] args) throws InterruptedException {
        final VolatileAtomicityDemo demo = new VolatileAtomicityDemo();

        // 创建 10 个线程,每个线程对 count 自增 1000 次
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.increment();
                }
            });
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (Thread t : threads) {
            t.join();
        }

        // 结果几乎总是小于 10000
        System.out.println("Final count (volatile): " + demo.count);
    }
}

正确保证原子性的方案:使用 AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicSolutionDemo {
    // 使用原子类保证原子性
    private AtomicInteger atomicCount = new AtomicInteger(0);

    public void safeIncrement() {
        atomicCount.incrementAndGet(); // 这是一个原子操作
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicSolutionDemo demo = new AtomicSolutionDemo();
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    demo.safeIncrement();
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        // 结果总是 10000
        System.out.println("Final count (AtomicInteger): " + demo.atomicCount.get());
    }
}

对比分析与最佳实践

特性volatilesynchronized / LockAtomicInteger 等原子类
原子性不保证保证保证(针对单一变量)
可见性保证保证(解锁前将变量刷回主内存)保证
有序性保证(禁止重排序)保证(as-if-serial 和管程锁定规则)内部通过 volatile 和 CAS 保证
性能影响很低,仅内存屏障较高,涉及内核态切换和线程阻塞中等,基于 CPU 的 CAS 指令,无锁,高并发下性能好
适用场景状态标志位(如 while (!stop))、单次安全发布(如双重检查锁)、读远多于写的简单共享变量需要保证一段代码块多个变量操作原子性的复杂场景需要对单个变量进行原子操作的计数器、累加器等场景

常见误区

  • 误区一:认为 volatile 是轻量级的锁,可以替代 synchronized 用于线程安全。错! 它无法替代锁,因为它不提供互斥访问和原子性。
  • 误区二:过度使用 volatile。对于不涉及共享或本身已是原子(如 longdouble 在 64 位 JVM 上的写入)的操作,无需使用。

总结

volatile 是并发编程中的 “轻量级同步剂”,它提供了卓越的可见性和有序性保障,但它的能力边界非常明确:绝不提供原子性。要保证复合操作的原子性,必须借助锁(synchronized/Lock)或无锁的原子类(AtomicXxx)。