第4章

⚡ volatile关键字

深入理解Java轻量级同步机制的内存语义和应用场景

学习目标

volatile原理

volatile是Java提供的轻量级同步机制,它保证了变量的可见性和禁止指令重排序,但不保证原子性。理解volatile的工作原理对于编写高效的并发程序至关重要。

核心特性

volatile关键字提供了比synchronized更轻量级的同步机制,主要解决变量在多线程环境下的可见性问题。

volatile的三大特性

可见性保证
当一个线程修改volatile变量时,其他线程能够立即看到这个修改,确保变量在多线程间的可见性。
禁止重排序
编译器和处理器不会对volatile变量的读写操作进行重排序,保证程序执行的有序性。
非原子性
volatile不保证复合操作的原子性,如i++操作仍然需要额外的同步措施。

底层实现机制

volatile的实现依赖于内存屏障(Memory Barrier)和缓存一致性协议。当声明一个变量为volatile时,JVM会在适当的位置插入内存屏障指令,确保:

volatile变量示例
public class VolatileExample {
    private volatile boolean flag = false;
    private int count = 0;
    
    public void writer() {
        count = 42;        // 1
        flag = true;       // 2 volatile写
    }
    
    public void reader() {
        if (flag) {        // 3 volatile读
            int i = count; // 4 一定能看到count=42
        }
    }
}

内存屏障

内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的内存操作顺序和可见性。volatile的语义正是通过在适当位置插入内存屏障来实现的。

四种内存屏障类型

LoadLoad屏障
确保屏障前的读操作完成后,才能执行屏障后的读操作。防止读操作重排序。
LoadStore屏障
确保屏障前的读操作完成后,才能执行屏障后的写操作。
StoreLoad屏障
确保屏障前的写操作完成后,才能执行屏障后的读操作。开销最大的屏障。
StoreStore屏障
确保屏障前的写操作完成后,才能执行屏障后的写操作。防止写操作重排序。

volatile的内存屏障插入策略

插入规则
  • volatile写前:插入StoreStore屏障
  • volatile写后:插入StoreLoad屏障
  • volatile读后:插入LoadLoad和LoadStore屏障
内存屏障示意图
// 写操作的内存屏障
StoreStore屏障
volatile写操作
StoreLoad屏障

// 读操作的内存屏障
volatile读操作
LoadLoad屏障
LoadStore屏障

happens-before规则

happens-before是JMM(Java内存模型)中的一个重要概念,用于描述两个操作之间的内存可见性关系。如果操作A happens-before操作B,那么A的执行结果对B可见。

主要的happens-before规则

程序顺序规则
在一个线程中,按照程序代码顺序,书写在前面的操作happens-before书写在后面的操作。
volatile规则
对一个volatile变量的写操作happens-before后续对这个变量的读操作。
传递性规则
如果A happens-before B,B happens-before C,那么A happens-before C。
重要提醒

happens-before关系不等同于时间上的先后关系,它主要描述的是内存可见性的保证。

happens-before示例
public class HappensBeforeExample {
    private int a = 0;
    private volatile boolean flag = false;
    
    public void writer() {
        a = 1;          // 1
        flag = true;    // 2
    }
    
    public void reader() {
        if (flag) {     // 3
            int i = a;  // 4 一定能看到a=1
        }
    }
    
    // 根据happens-before规则:
    // 1 happens-before 2 (程序顺序规则)
    // 2 happens-before 3 (volatile规则)
    // 3 happens-before 4 (程序顺序规则)
    // 因此 1 happens-before 4 (传递性)
}

双重检查锁定

双重检查锁定(Double-Checked Locking,DCL)是一种常用的单例模式实现方式,但在没有volatile的情况下存在严重的并发问题。

DCL模式的问题

并发问题

在没有volatile的DCL实现中,由于指令重排序的存在,可能导致其他线程获得一个未完全初始化的对象实例。

有问题的DCL实现
public class UnsafeSingleton {
    private static UnsafeSingleton instance;
    
    public static UnsafeSingleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (UnsafeSingleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new UnsafeSingleton(); // 问题所在
                }
            }
        }
        return instance;
    }
}

问题在于 new UnsafeSingleton() 这行代码实际上包含三个步骤:

由于指令重排序,步骤2和3可能被交换,导致instance指向了一个未初始化的对象。

volatile解决方案

正确的DCL实现
public class SafeSingleton {
    private static volatile SafeSingleton instance;
    
    public static SafeSingleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (SafeSingleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new SafeSingleton(); // 现在是安全的
                }
            }
        }
        return instance;
    }
    
    private SafeSingleton() {
        // 私有构造函数
    }
}
解决原理

通过将instance声明为volatile,禁止了指令重排序,确保对象完全初始化后才会被赋值给instance变量。

volatile vs synchronized

volatile和synchronized都是Java中的同步机制,但它们有着不同的特点和适用场景。正确理解它们的区别有助于选择合适的同步策略。

特性对比

volatile特点
  • 轻量级同步机制
  • 保证可见性和有序性
  • 不保证原子性
  • 无阻塞特性
  • 性能开销小
synchronized特点
  • 重量级同步机制
  • 保证可见性、有序性和原子性
  • 互斥访问
  • 可能导致线程阻塞
  • 性能开销较大

使用场景选择

选择策略
  • 使用volatile:状态标志、单例模式的DCL、读多写少的场景
  • 使用synchronized:复合操作、临界区保护、需要原子性的场景
使用场景示例
public class SyncComparison {
    // 适合volatile的场景:状态标志
    private volatile boolean shutdown = false;
    
    public void shutdown() {
        shutdown = true;
    }
    
    public void doWork() {
        while (!shutdown) {
            // 执行工作
        }
    }
    
    // 需要synchronized的场景:复合操作
    private int count = 0;
    
    public synchronized void increment() {
        count++; // 复合操作,需要原子性
    }
    
    public synchronized int getCount() {
        return count;
    }
}

性能对比

在性能方面,volatile通常比synchronized有更好的表现,特别是在读操作频繁的场景下。但需要注意的是,volatile不能替代synchronized的所有功能。

实践建议

正确使用volatile

适用场景
  • 状态标志变量
  • 单例模式的DCL
  • 读多写少的共享变量
  • 发布-订阅模式中的状态变量
不适用场景
  • 复合操作(如i++)
  • 需要原子性的操作
  • 依赖当前值的更新
  • 多个变量的一致性保证
常见误区
  • 认为volatile可以保证原子性
  • 过度使用volatile导致性能问题
  • 忽视volatile的内存屏障开销
  • 在不需要的地方使用volatile

最佳实践

上一章:synchronized关键字 返回目录 下一章:Lock接口