volatile 关键字如何保证代码的有序性执行?请举例说明。

参考回答

volatile 关键字通过 内存屏障(Memory Barrier)指令重排序规则,保证了代码在多线程环境下的有序性执行。

  1. 内存屏障
    • 编译器和 CPU 会在 volatile 修饰的变量的读写操作前后插入内存屏障,禁止特定的指令重排序。
    • 写操作内存屏障:在写入 volatile 变量后,强制将变量刷新到主内存,同时禁止之前的写操作重排序到 volatile 写操作之后。
    • 读操作内存屏障:在读取 volatile 变量时,强制线程从主内存中获取最新值,同时禁止之后的读操作重排序到 volatile 读操作之前。
  2. 指令重排序规则
    • JMM(Java 内存模型)规定:volatile 变量的操作与普通变量的操作之间不能发生重排序。
    • 通过内存屏障约束读写操作的顺序,确保代码执行的有序性。

示例:volatile 保证有序性

1. 问题:指令重排序导致的结果不一致

代码示例:

class ReorderingExample {
    private int value = 0;
    private boolean flag = false;

    public void writer() {
        value = 42;  // 写普通变量
        flag = true; // 写普通变量
    }

    public void reader() {
        if (flag) { // 读取普通变量
            System.out.println("Value: " + value); // 可能输出 0
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ReorderingExample example = new ReorderingExample();

        Thread writerThread = new Thread(example::writer);
        Thread readerThread = new Thread(example::reader);

        writerThread.start();
        readerThread.start();
    }
}

可能输出:

Value: 0

原因:

  • 由于指令重排序,writer 方法中的 value = 42flag = true 顺序可能颠倒。
  • reader 方法可能看到 flag = true,但此时 value 的写操作尚未完成,读取到的是初始值 0

2. 使用 volatile 修复有序性问题

flag 变量声明为 volatile,代码如下:

class ReorderingExample {
    private int value = 0;
    private volatile boolean flag = false;

    public void writer() {
        value = 42;  // 写普通变量
        flag = true; // 写 volatile 变量
    }

    public void reader() {
        if (flag) { // 读取 volatile 变量
            System.out.println("Value: " + value); // 一定输出 42
        }
    }
}

保证有序性原因:

  • 写入 flag 时,volatile 的写屏障确保 value = 42 操作完成后才会执行 flag = true
  • 读取 flag 时,volatile 的读屏障确保在读取 flag 之前,所有变量(包括 value)都已从主内存中刷新到线程工作内存。

输出:

Value: 42

3. 内存屏障的作用

volatile 在底层插入了内存屏障,具体如下:

  1. 写操作内存屏障(Store Barrier)
  • 在 volatile变量写入后插入 StoreStore和 StoreLoad屏障:
    • StoreStore:保证在 volatile 写操作之前的普通写操作不会重排序到 volatile 写操作之后。
    • StoreLoad:防止写后读重排序。
  1. 读操作内存屏障(Load Barrier)
  • 在 volatile变量读取前插入 LoadLoad和 LoadStore屏障:
    • LoadLoad:保证在 volatile 读操作之后的普通读操作不会被重排序到 volatile 读操作之前。
    • LoadStore:防止读后写重排序。

拓展:volatile 与指令重排序的双重检查锁定

volatile双重检查锁定(DCL,Double-Checked Locking) 中的作用,典型场景是实现线程安全的单例模式:

问题:指令重排序破坏单例

代码示例(有问题的实现):

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

问题:

  • instance = new Singleton()
    

    是一个非原子操作,分解为以下三步:

    1. 分配内存。
    2. 调用构造器初始化对象。
    3. 将对象引用赋值给 instance
  • 重排序可能导致步骤 2 和步骤 3 交换顺序:
    • 一个线程可能看到 instance 不为 null(步骤 3 完成),但访问的是未初始化的对象(步骤 2 未完成)。

解决:volatile 禁止重排序

正确实现:

class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

作用:

  • volatile 禁止 instance = new Singleton() 的指令重排序,确保对象在赋值前已完成初始化。
  • 保证了 Singleton 的线程安全。

总结

  1. volatile 如何保证有序性?
    • 通过内存屏障,禁止 volatile 修饰变量的读写操作与其他操作重排序。
    • 在写入 volatile 变量时,确保之前的写操作先于 volatile 写操作完成。
    • 在读取 volatile 变量时,确保之后的读操作晚于 volatile 读操作执行。
  2. 实际场景
    • 解决指令重排序问题,如多线程通信、双重检查锁定单例模式。
    • 确保多线程环境下的有序执行,避免难以调试的并发问题。

发表评论

后才能评论