volatile 关键字可以保证原子性吗?
2026年01月13日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/
面试考察点
面试官问这个问题,核心是想考察:
- 对
volatile关键字核心语义的准确理解:你是否清楚volatile能保证什么(可见性、有序性),不能保证什么(原子性)。这是基础。 - 对“原子性”概念的理解深度:面试官不仅仅想知道你是否能背出“不能”,更想知道你是否理解原子性在并发编程中的具体含义(例如,一个或多个操作不可分割的整体)。
- 对 JMM(Java内存模型)的实践认知:通过这个问题,可以引出你对共享变量多线程访问、工作内存与主内存交互等底层机制的理解。
- 解决实际问题的能力:当你指出
volatile的不足后,面试官通常会跟进问:“那么如何保证原子性?”,以此考察你是否了解synchronized、Lock或java.util.concurrent.atomic包下的原子类等正确方案。
核心答案
不能。 volatile 关键字无法保证原子性。
它只能保证两大特性:
- 可见性:对一个
volatile变量的写操作,会立即刷新到主内存,并导致其他线程中该变量的缓存失效,从而保证读取到最新值。 - 有序性(禁止指令重排序):通过插入内存屏障,防止 JVM 和处理器对
volatile变量读写操作进行重排序。
典型的反例是 i++(自增)操作。即使 i 被声明为 volatile,i++ 这个 “读取-修改-写入” 组合操作在多线程环境下依然不是原子的,仍会导致计数错误。
深度解析
原理/机制
为什么 volatile 管不了原子性?这需要从 JMM 和操作本身来分析。
- 原子性的定义:一个或多个操作作为一个不可分割的整体,要么全部执行成功,要么全部不执行,中间状态对外不可见。
- 复合操作的困境:像
i++、flag = !flag这类操作,在字节码和 CPU 指令层面通常由多个步骤组成(例如i++包含:读取i的值、给i加 1、将新值写回i)。volatile能保证每一步的读或写是可见且有序的,但它无法将这三个步骤“捆绑”成一个原子操作。 - 问题发生场景:当线程 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());
}
}
对比分析与最佳实践
| 特性 | volatile | synchronized / Lock | AtomicInteger 等原子类 |
|---|---|---|---|
| 原子性 | 不保证 | 保证 | 保证(针对单一变量) |
| 可见性 | 保证 | 保证(解锁前将变量刷回主内存) | 保证 |
| 有序性 | 保证(禁止重排序) | 保证(as-if-serial 和管程锁定规则) | 内部通过 volatile 和 CAS 保证 |
| 性能影响 | 很低,仅内存屏障 | 较高,涉及内核态切换和线程阻塞 | 中等,基于 CPU 的 CAS 指令,无锁,高并发下性能好 |
| 适用场景 | 状态标志位(如 while (!stop))、单次安全发布(如双重检查锁)、读远多于写的简单共享变量 | 需要保证一段代码块或多个变量操作原子性的复杂场景 | 需要对单个变量进行原子操作的计数器、累加器等场景 |
常见误区:
- 误区一:认为
volatile是轻量级的锁,可以替代synchronized用于线程安全。错! 它无法替代锁,因为它不提供互斥访问和原子性。 - 误区二:过度使用
volatile。对于不涉及共享或本身已是原子(如long、double在 64 位 JVM 上的写入)的操作,无需使用。
总结
volatile 是并发编程中的 “轻量级同步剂”,它提供了卓越的可见性和有序性保障,但它的能力边界非常明确:绝不提供原子性。要保证复合操作的原子性,必须借助锁(synchronized/Lock)或无锁的原子类(AtomicXxx)。