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 分别能保证哪些。

  2. JMM 理解深度:这道题背后考察的是你对 Java 内存模型(JMM)的理解。如果只知道 "volatile 能保证可见性" 这个结论,却说不出内存屏障、主内存与工作内存的交互过程,面试官会觉得你只是背了八股文。

  3. 实战避坑能力:考察你是否知道在什么场景下 volatile 不够用,需要用锁或原子类来替代。这个在实际开发中真的会踩坑,我就见过有同事用 volatile 修饰计数器,结果线上数据对不上。

核心答案

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

它只能保证以下两个特性:

特性volatile 是否保证说明
可见性✅ 保证一个线程修改了变量,其他线程立刻能看到最新值
有序性✅ 部分保证禁止指令重排序(通过内存屏障)
原子性❌ 不保证复合操作(如 i++)依然可能出现并发问题

深度解析

一、为什么 volatile 不能保证原子性——用 i++ 说话

直接上代码,这是最经典的反例:

public class VolatileAtomicTest {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                count++;  // 看似一行代码,实际不是原子操作
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        // 期望 20000,实际结果几乎每次都小于 20000
        System.out.println("count = " + count);
    }
}

你觉得加了 volatile 结果就是 20000?跑一下就知道了,几乎每次都小于 20000。为什么?

因为 count++ 不是一步操作,它实际包含了三步:

上图展示了 count++ 的完整执行过程,它由三个独立的操作组成。虽然 volatile 保证了每次读取都从主内存拿最新值、每次写入都立刻刷回主内存,但它无法保证这三步作为一个整体不被打断。来看一个经典的竞态场景:

上图展示了一个典型的 "丢失更新" 问题。整个过程的关键在于:

  • T1:Thread-1 从主内存读取 count=0,此时 volatile 保证它拿到的是最新值,没问题
  • T2:Thread-2 也从主内存读取 count=0,注意此时 Thread-1 还没写回,所以 Thread-2 拿到的依然是 0
  • T3-T4:两个线程各自在自己的工作内存中做 0+1=1 的计算
  • T5:Thread-1 把结果 1 写回主内存
  • T6:Thread-2 也把结果 1 写回主内存,覆盖了 Thread-1 写入的 1

两次自增,结果只增加了 1。volatile 确实保证了每次写入都能立刻对其他线程可见,但它管不了 "读-改-写" 之间被别的线程插队。这就是为什么 volatile 不能保证原子性。

二、那什么场景下 volatile 够用?

volatile 适用于 一个线程写、多个线程读 的场景,或者状态标记位这种 "纯写入" 的场景。

// 场景一:状态标记位(完全 OK)
private volatile boolean running = true;

public void stop() {
    running = false;  // 单纯的赋值操作,本身就是原子的
}

public void doWork() {
    while (running) {
        // 业务逻辑
    }
}
// 场景二:双重检查锁定(DCL)单例模式
public class Singleton {
    private static volatile Singleton instance;  // volatile 防止指令重排序

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

DCL 单例中 volatile 的作用不是保证原子性,而是防止 new Singleton() 的指令重排序。对象创建分三步:分配内存 → 初始化对象 → 将引用指向内存地址。如果不加 volatile,JVM 可能把步骤 2 和 3 重排序,导致其他线程拿到了一个还没初始化完的 "半成品" 对象。

三、需要原子性怎么办?

既然 volatile 不行,那该用什么?

方案适用场景示例
AtomicInteger 等原子类单个变量的原子操作AtomicInteger.incrementAndGet()
synchronized代码块的原子性保护包裹复合操作
ReentrantLock需要更灵活的锁控制tryLock()、超时获取等
// 方案一:用 AtomicInteger(推荐)
private static AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();  // 底层 CAS,无锁原子操作

// 方案二:用 synchronized( heavier,但通用)
private static int count = 0;

synchronized (VolatileAtomicTest.class) {
    count++;
}

AtomicInteger 底层用的是 CAS(Compare And Swap),这是一种无锁的原子操作,性能比 synchronized 好很多,是计数器场景的首选。

面试高频追问

  1. 追问一:volatile 是怎么保证可见性的?

    通过内存屏障(Memory Barrier)实现。写操作前插入 StoreStore 屏障,写操作后插入 StoreLoad 屏障;读操作前插入 LoadLoad 屏障,读操作后插入 LoadStore 屏障。这些屏障强制 CPU 将写缓冲区的数据刷新到主内存,并使其他 CPU 缓存行失效,从而保证可见性。

  2. 追问二:什么是 CAS?ABA 问题怎么解决?

    CAS 是 "比较并交换",包含三个操作数:内存值 V、预期值 A、新值 B。只有当 V == A 时,才将 V 更新为 B,否则重试。ABA 问题是指值从 A 变成 B 又变回 A,CAS 无法感知中间的变化。解决方案是用 AtomicStampedReference(带版本号)或 AtomicMarkableReference(带标记位)。

  3. 追问三:volatile 和 synchronized 的区别?

    volatile 是轻量级的同步机制,只能修饰变量,保证可见性和有序性,不保证原子性;synchronized 是重量级的,能修饰方法和代码块,同时保证可见性、有序性和原子性。能用 volatile 解决的就用 volatile,解决不了再用 synchronized

常见面试变体

  • "volatile 能保证线程安全吗?"
  • "volatile 和 synchronized 有什么区别?"
  • "i++ 是原子操作吗?加了 volatile 之后呢?"
  • "什么场景下用 volatile 就够了?"

记忆口诀

volatile 三板斧:可见性 ✅、有序性 ✅、原子性 ❌。复合操作还得靠锁或 CAS。

总结

volatile 不能保证原子性,它只管两件事——可见性(改了立刻让别人看到)和有序性(禁止指令重排)。遇到 i++ 这种 "读-改-写" 的复合操作,volatile 无能为力,得用 AtomicInteger(CAS)或 synchronized 来保证原子性。面试中把这个区别说清楚,基本就不会在这道题上翻车了。