核心机制

volatile关键字通过两个底层机制解决多线程可见性问题:

  1. 强制刷新与读取主内存:保证可见性
  2. 插入内存屏障:禁止指令重排序

一、保证可见性的原理

1. JMM内存模型

线程A工作内存        主内存        线程B工作内存
┌─────────┐                       ┌─────────┐
│ flag拷贝 │                       │ flag拷贝 │
│ (false) │                       │ (false) │
└─────────┘                       └─────────┘
     ↑               ↑               ↑
     └───────────────┴───────────────┘
           volatile flag = false

问题:线程A修改flag,可能只更新工作内存,线程B看不到。

volatile解决

volatile boolean flag = false;

// 线程A写入
flag = true;
// ↓ volatile写:立即刷新到主内存,并使其他CPU缓存失效

// 线程B读取
if (flag) {
    // ↓ volatile读:强制从主内存读取最新值
}

2. 硬件层面(MESI协议)

CPU0                  主内存              CPU1
Cache: flag=false  ←→ flag=false ←→   Cache: flag=false
  ↓ (写入true)
Cache: flag=true (Modified)
  ↓ 发送Invalidate消息
                                       Cache: flag=? (Invalid)
                                         ↓ 读取flag
                                       从主内存重新加载: flag=true

MESI状态

  • M (Modified):当前CPU独占且已修改
  • E (Exclusive):当前CPU独占但未修改
  • S (Shared):多个CPU共享
  • I (Invalid):缓存无效

二、禁止重排序的原理

1. 内存屏障的插入策略

class VolatileBarriers {
    int data = 0;
    volatile boolean ready = false;
    
    // 写线程
    public void writer() {
        data = 42;              // 普通写
        // ─────────────────────────
        // StoreStore屏障
        // ─────────────────────────
        ready = true;           // volatile写
        // ─────────────────────────
        // StoreLoad屏障(最强,最耗性能)
        // ─────────────────────────
    }
    
    // 读线程
    public void reader() {
        // ─────────────────────────
        // LoadLoad屏障
        // ─────────────────────────
        if (ready) {            // volatile读
            // ─────────────────────────
            // LoadStore屏障
            // ─────────────────────────
            System.out.println(data);  // 保证看到42
        }
    }
}

2. happens-before语义

// volatile的happens-before规则
volatile int v = 0;
int a = 0;

// 线程A
a = 1;          // 操作1
v = 1;          // 操作2(volatile写)

// 线程B
if (v == 1) {   // 操作3(volatile读)
    int b = a;  // 操作4,保证b=1
}

// happens-before链:
// 1 happens-before 2(程序顺序规则)
// 2 happens-before 3(volatile规则)
// 3 happens-before 4(程序顺序规则)
// 传递性:1 happens-before 4

三、综合案例

案例1:双重检查锁单例

public class Singleton {
    // volatile保证:可见性 + 禁止重排序
    private static volatile Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {              // 读1(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {      // 读2(加锁)
                    instance = new Singleton();  // 写
                }
            }
        }
        return instance;
    }
}

volatile的双重作用

  1. 禁止重排序: ```java // 对象创建的三步: memory = allocate(); // 1. 分配内存 ctorInstance(memory); // 2. 初始化 instance = memory; // 3. 赋值引用

// volatile禁止2和3重排序,避免返回半初始化对象


2. **保证可见性**:
```java
// 线程A创建对象后,volatile确保instance立即对其他线程可见
// 线程B读取instance时,能看到完全初始化的对象

案例2:生产者-消费者

public class ProducerConsumer {
    private String data;
    private volatile boolean ready = false;
    
    // 生产者
    public void produce() {
        data = "Hello World";   // 1. 准备数据
        ready = true;           // 2. volatile写
        // StoreStore屏障确保1在2之前完成
    }
    
    // 消费者
    public void consume() {
        while (!ready) {        // 3. volatile读
            // 自旋等待
        }
        // LoadLoad屏障确保4在3之后执行
        System.out.println(data);  // 4. 使用数据,保证看到"Hello World"
    }
}

案例3:状态标志

public class Server {
    private volatile boolean running = true;
    
    // 主线程
    public void run() {
        while (running) {  // volatile读,保证及时看到shutdown的修改
            // 处理请求
            processRequest();
        }
        cleanup();
    }
    
    // 其他线程
    public void shutdown() {
        running = false;  // volatile写,立即对run()线程可见
    }
}

四、性能考量

开销对比

// JMH基准测试
public class VolatilePerformance {
    int normalVar = 0;
    volatile int volatileVar = 0;
    AtomicInteger atomicVar = new AtomicInteger(0);
    
    @Benchmark
    public void testNormal() {
        normalVar++;     // ~1ns(非线程安全)
    }
    
    @Benchmark
    public void testVolatile() {
        volatileVar++;   // ~10-20ns(可见性 + 重排序保护)
    }
    
    @Benchmark
    public void testAtomic() {
        atomicVar.incrementAndGet();  // ~20-50ns(原子性 + CAS自旋)
    }
}

使用建议

场景 推荐方案 原因
状态标志 volatile 轻量级,无锁
计数器 AtomicInteger 保证原子性
单次发布对象 volatile DCL模式
复合操作 synchronized 保证原子性

五、常见误区

误区1:volatile保证原子性

// 错误
volatile int count = 0;
count++;  // 非原子!包含"读-改-写"三步

// 正确
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

误区2:volatile万能

// 错误:多个变量无法保证一致性
volatile int x = 0;
volatile int y = 0;

x++;
y++;  // 中间可能被其他线程打断

// 正确:使用锁
synchronized (lock) {
    x++;
    y++;
}

误区3:不需要volatile

// 错误:以为JVM会自动同步
boolean stop = false;
while (!stop) { ... }  // 可能永远看不到修改

// 正确
volatile boolean stop = false;

答题总结

核心要点

  1. 可见性机制
    • volatile写:立即刷新到主内存
    • volatile读:强制从主内存读取
    • MESI协议使其他CPU缓存失效
  2. 禁止重排序
    • volatile写前插入StoreStore屏障
    • volatile写后插入StoreLoad屏障
    • volatile读后插入LoadLoad + LoadStore屏障
  3. happens-before
    • volatile写 happens-before volatile读
    • 传递性保证前后操作的可见性

面试答题模板

“volatile通过两个机制解决多线程可见性和重排序问题。

可见性方面,volatile变量的写操作会立即刷新到主内存,读操作会强制从主内存加载。底层利用CPU的MESI缓存一致性协议,当一个CPU修改volatile变量时,会使其他CPU的缓存行失效,强制它们重新从主内存读取。

禁止重排序方面,JMM会在volatile操作前后插入内存屏障。volatile写之前插入StoreStore屏障,确保之前的普通写不会被重排序到volatile写之后;写之后插入StoreLoad屏障,确保volatile写不会与之后的读操作重排序。volatile读之后插入LoadLoad和LoadStore屏障,确保volatile读与后续操作不会重排序。

典型应用是双重检查锁的单例模式,volatile既保证instance的可见性,又禁止对象创建过程的重排序,避免返回半初始化对象。

但要注意volatile不保证原子性,像i++这种复合操作仍需要用AtomicInteger或synchronized。”