volatile
并发世界里,volatile 是最轻量的同步机制。
但很多人用它用错了——以为加了 volatile 就线程安全了,结果踩了坑还不知道。
volatile 能做什么
volatile 做两件事:
- 保证可见性:写操作立即刷新到主内存,读操作立即从主内存读取
- 防止指令重排序:阻止编译器和 CPU 的优化重排
但它不保证原子性——i++ 这种复合操作,就算加了 volatile 也不行。
┌─────────────────────────────────────────────┐
│ volatile 的三个作用 │
├─────────────────────────────────────────────┤
│ │
│ 1. 可见性: │
│ 写 → 立刻刷主内存 │
│ 读 → 立刻读主内存 │
│ │
│ 2. 有序性: │
│ 防止指令重排序 │
│ │
│ 3. 不保证原子性: │
│ i++ 仍是三步操作,volatile 保护不了 │
│ │
└─────────────────────────────────────────────┘基本用法
状态标志位(volatile 的经典场景)
java
public class ShutdownFlagDemo {
private volatile boolean running = true;
public void run() {
Thread worker = new Thread(() -> {
while (running) {
try {
System.out.println("工作中...");
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("线程停止");
});
worker.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
running = false; // 写入后立即被 worker 线程看到
System.out.println("请求停止");
}
public static void main(String[] args) throws InterruptedException {
new ShutdownFlagDemo().run();
}
}没有 volatile,主线程设置 running=false 后,worker 线程可能永远看不到(因为 JIT 优化成了 while(true))。
单次初始化模式
java
public class VolatileInitDemo {
private volatile boolean initialized = false;
private volatile String resource;
public void init() {
if (!initialized) {
synchronized (this) {
if (!initialized) {
resource = loadResource();
initialized = true;
}
}
}
}
public void use() {
if (initialized) {
System.out.println("使用资源: " + resource);
}
}
private String loadResource() {
return "已加载的资源";
}
public static void main(String[] args) {
VolatileInitDemo demo = new VolatileInitDemo();
demo.init();
demo.use();
}
}这里 volatile 的作用:防止 initialized 和 resource 的赋值顺序被重排,确保其他线程看到的是完整初始化后的状态。
观察 volatile 的效果
java
public class VolatileEffectDemo {
// 普通变量
private static boolean normalFlag = false;
// volatile 变量
private static volatile boolean volFlag = false;
public static void main(String[] args) throws InterruptedException {
// 测试非 volatile
System.out.println("=== 普通变量测试 ===");
testFlag("normalFlag", () -> normalFlag = true,
() -> { while (!normalFlag) { Thread.yield(); } });
// 测试 volatile
System.out.println("\n=== volatile 变量测试 ===");
testFlag("volFlag", () -> volFlag = true,
() -> { while (!volFlag) { Thread.yield(); } });
}
private static void testFlag(String name, Runnable setter, Runnable waiter) {
long start = System.nanoTime();
Thread writer = new Thread(setter);
Thread reader = new Thread(waiter);
reader.start();
writer.start();
writer.join();
reader.join();
long elapsed = (System.nanoTime() - start) / 1_000_000;
System.out.println(name + " 可见延迟: " + elapsed + "ms");
}
}在多核环境下,普通变量可能出现明显的可见延迟。
Double-Checked Locking:volatile 的经典应用
这是面试超高频题,单例模式中的 volatile:
java
public class Singleton {
// 如果没有 volatile,以下问题可能发生:
// 1. 分配内存
// 2. 调用构造函数(创建对象)
// 3. 将引用赋值给 instance
// CPU 可能重排序为 1 -> 3 -> 2
// 其他线程在第2步完成前就看到了非null的instance
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 同步
if (instance == null) { // 第二次检查
instance = new Singleton(); // 创建
}
}
}
return instance;
}
private Singleton() {}
}为什么需要两次检查
- 第一次检查(if):避免不必要的同步。如果实例已经创建,直接返回,不需要抢锁
- 第二次检查(if):防止多线程同时通过第一次检查,都去创建实例
为什么需要 volatile
防止 new Singleton() 的指令重排序。如果 2 和 3 重排:
线程A: instance = new Singleton(); // 1.分配内存 3.赋值引用(instance!=null)
// 2.构造函数还没执行
线程B: if (instance == null) // false,跳过
instance.doSomething(); // ❌ 访问未初始化的对象!volatile 防止重排序,确保 1 -> 2 -> 3 的顺序。
计数器:volatile 的禁区
java
public class VolatileCounter {
// ❌ 错误:volatile 不保证原子性
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
int threadCount = 100;
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter++; // 非原子操作!volatile 只保证读写,不保证复合操作
}
}).start();
}
Thread.sleep(2000);
System.out.println("期望: " + (threadCount * 1000));
System.out.println("实际: " + counter); // 小于期望值!
}
}这里 counter++ 的三个步骤(读、改、写)不是原子的,volatile 只能保证每次读取和写入是最新的,但两个线程同时 ++ 时仍然会丢失更新。
正确做法:AtomicInteger
java
// ✅ 正确
private static final AtomicInteger counter = new AtomicInteger(0);
// 使用
counter.incrementAndGet(); // 原子操作volatile vs synchronized vs Atomic*
| 维度 | volatile | synchronized | AtomicInteger |
|---|---|---|---|
| 可见性 | ✅ | ✅ | ✅ |
| 原子性 | ❌ | ✅ | ✅ |
| 防止重排序 | ✅ | ✅ | ❌ |
| 阻塞 | 否 | 是 | 否(CAS 自旋) |
| 性能 | 最高 | 低 | 中 |
适用场景
volatile 适合的场景:
- 只有一个线程写、多个线程读的状态标志
- 单例模式中的对象引用
- 配合 happens-before 规则传递状态
不适合的场景:
- 计数器(用 AtomicInteger)
- 需要复合操作(用 synchronized 或 Atomic*)
- 需要原子性保证的操作
注意事项
- volatile 不能替代 synchronized:只保证可见性和有序性,不保证原子性
- 对变量的写入不依赖当前值:如
flag = true可以,counter++不行 - 没有死循环风险:如果写入依赖当前值,可能出现可见性问题
- 64位变量:JDK 5+ 的 volatile 保证 long/double 读写的原子性
要点回顾
volatile保证可见性和有序性,不保证原子性- 适用于:状态标志、单例模式、配合 happens-before 传递状态
- 不适用于:计数器、复合操作
- 单例模式中
volatile防止指令重排序导致访问未初始化对象
相关链接
- 可见性 - volatile 的核心作用
- 原子性 - 为什么 volatile 不能替代 synchronized
- Happens-Before - 理解 volatile 的语义
- 指令重排序 - 有序性问题
