Skip to content

Volatile

1. 防止指令重排序 (Prevent Reordering)

  • 问题: 指令重排序可能导致多线程环境下程序出现意想不到的结果,尤其是在对象实例化等场景。

  • 示例:双重检查锁 (DCL) 单例模式

    public class Singleton { public static volatile Singleton singleton; // 使用 volatile 关键字 private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }

    • 原因: singleton = new Singleton(); 对象实例化通常分为三个步骤:
      1. 分配内存空间。
      2. 初始化对象。
      3. 将内存地址赋值给引用 singleton
    • 重排序风险: 指令重排序可能导致步骤 2 和 3 颠倒顺序,即先将未初始化的对象引用赋值给 singleton,此时如果另一个线程访问 getInstance(),可能获得未完成初始化的对象,导致错误。
    • volatile 的作用: volatile 关键字禁止 singleton 变量赋值时的指令重排序,确保对象初始化完成后,引用才被其他线程可见。

2. 实现可见性 (Visibility)

  • 问题: 线程拥有自己的工作内存 (高速缓存),对共享变量的修改可能不会立即同步到主内存,导致其他线程读取到过期的值,产生可见性问题

  • volatile 的作用: volatile 关键字强制线程每次使用变量时都从主内存中读取,每次修改后都立即写回主内存,保证所有线程看到的是变量的最新值。

  • 示例:

    ``` public class TestVolatile { private static volatile boolean stop = false; // 使用 volatile 关键字

    public static void main(String[] args) {
        new Thread("Thread A") {
            @Override
            public void run() {
                while (!stop) { // 线程 A 循环读取 stop 变量
                }
                System.out.println(Thread.currentThread() + " stopped");
            }
        }.start();
    
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread() + " after 1 seconds");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop = true; // 线程 Main 修改 stop 变量
    }
    

    } ```

    • 结果: 加上 volatile 后,线程 Main 修改 stop 的值后,线程 A 能立即看到修改,并停止循环。

3. 保证原子性 (Atomicity): 单次读/写

  • 局限性: volatile 不能保证复合操作的原子性,例如 i++

  • i++ 非原子性示例:

    ``` public class VolatileTest01 { volatile int i; // volatile 变量 i

    public void addI(){
        i++; // i++ 复合操作
    }
    
    public static void main(String[] args) throws InterruptedException {
        final  VolatileTest01 test01 = new VolatileTest01();
        for (int n = 0; n < 1000; n++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test01.addI();
                }
            }).start();
        }
        Thread.sleep(10000);
        System.out.println(test01.i); // 结果通常小于 1000
    }
    

    } ```

    • 原因: i++ 操作包含 读取-修改-写入 三个步骤,volatile 只能保证每次读写 i 的操作是原子的,但无法保证这三个步骤作为一个整体是原子的。
    • 解决方案: 使用 AtomicInteger 原子类或 synchronized 关键字来保证 i++ 的原子性。
    • longdouble 类型的原子性:

    • 对于 longdouble 类型,普通读写操作在某些平台下可能不是原子的,会被分成高 32 位和低 32 位两次操作。

    • volatile 可以保证 longdouble 类型的单次读写操作的原子性
    • 现代 JVM 实现: 大多数商用 JVM 已经将 64 位数据的读写操作作为原子操作,因此通常不需要特意将 longdouble 声明为 volatile

4. Volatile 的实现原理

volatile 的可见性和有序性是通过 内存屏障 (Memory Barrier) 实现的。

  • 内存屏障: CPU 指令,用于限制编译器和 CPU 的指令重排序,并保证缓存一致性
  • lock 前缀指令: volatile 变量写操作会被编译成汇编代码时,会多出一个 lock 前缀指令。lock 前缀指令会触发以下两个关键动作:
    1. 将当前处理器缓存行的数据写回系统内存 (主内存)。
    2. 使其他 CPU 缓存了该内存地址的数据失效。
  • 缓存一致性协议 (MESI): 用于保证多核处理器缓存之间数据一致性的协议。当一个处理器修改了共享变量,会通知其他处理器,使其缓存失效,需要重新从主内存读取数据。

5. Volatile 的 Happens-Before 关系

  • Volatile 变量规则: 对一个 volatile 域的写,Happens-Before 于任意后续对这个 volatile 域的读。

    ``` class VolatileExample { int a = 0; volatile boolean flag = false;

    public void writer() {
        a = 1;          // 1. 普通写
        flag = true;    // 2. volatile 写
    }
    
    public void reader() {
        if (flag) {     // 3. volatile 读
            int i = a;  // 4. 普通读
            // ...
        }
    }
    

    } ```

    • Happens-Before 关系:
      1. 程序顺序规则: 1 happens-before 2, 3 happens-before 4
      2. Volatile 变量规则: 2 happens-before 3
      3. 传递性规则: 1 happens-before 4
    • 保证: 线程 A 修改 volatile 变量 flagtrue 后,线程 B 读取 flag 时,能够看到最新的 flag 值,并且也能看到线程 A 在 flag = true 之前对 a = 1 的修改。

6. Volatile 的应用场景

使用 volatile 的条件:

  1. 写操作不依赖当前值: 例如,状态标志 boolean flag = true;,赋值操作 x = y;
  2. 变量不包含在与其他变量的不变式中: volatile 只能保证单个变量的可见性和有序性,无法保证多个变量之间的复合操作的原子性。
  3. 状态独立于程序其他部分: 变量的状态变化不影响程序其他逻辑的正确性。

常见使用模式:

  1. 状态标志 (Status Flag): 指示一次性事件是否发生,例如程序是否需要停止。

    volatile boolean shutdownRequested; public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // ... } }

  2. 一次性安全发布 (One-time Safe Publication): 安全发布只写一次,多线程读取的对象引用。

    public class BackgroundFloobleLoader { public volatile Flooble theFlooble; // volatile 对象引用 public void initInBackground() { theFlooble = new Flooble(); // 只写一次 } } public class SomeOtherClass { public void doWork() { if (floobleLoader.theFlooble != null) // 多线程读取 doSomething(floobleLoader.theFlooble); } }

  3. 独立观察 (Independent Observation): 定期发布观察结果供程序内部使用,例如温度传感器数据。

    public class UserManager { public volatile String lastUser; // volatile 变量记录最新用户名 public boolean authenticate(String user, String password) { if (passwordIsValid(user, password)) { lastUser = user; // 更新 volatile 变量 return true; } return false; } }

  4. Volatile Bean 模式: JavaBean 的所有数据成员都声明为 volatile,getter/setter 方法简单,对象引用成员指向不可变对象。

    @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; // ... getter and setter methods ... }

  5. 低开销的读-写锁策略 (Cheap Read-Write Lock Trick): 读多写少场景,结合 synchronized (写操作原子性) 和 volatile (读操作可见性) 优化性能。

    ``` @ThreadSafe public class CheesyCounter { @GuardedBy("this") private volatile int value; // volatile 变量

    public int getValue() { return value; } // 读操作 - volatile 读
    
    public synchronized int increment() { // 写操作 - synchronized 保证原子性
        return value++;
    }
    

    } ```

  6. 双重检查 (Double-Checked Locking): 单例模式的一种实现方式。

class Singleton {
    private volatile static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            syschronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    } 
}

1. volatile 关键字的作用是什么?

volatile 关键字是 Java 提供的一种轻量级的同步机制,主要有两个作用:

  • 可见性:
    • 确保被 volatile 修饰的变量,当一个线程修改了它的值,这个新值对于其他线程来说是立即可见的。
    • 也就是说,当一个线程写入 volatile 变量时,JVM 会强制将修改后的值立即写入主内存;当另一个线程读取该变量时,JVM 会强制其从主内存中读取最新值。
  • 有序性(禁止指令重排序):
    • 防止编译器和处理器对指令进行重排序优化。
    • volatile 关键字会插入内存屏障,保证特定的操作顺序,避免在多线程环境下出现意料之外的结果。

2. volatile 能保证原子性吗?

  • 不能。
  • volatile 只能保证可见性和有序性,但不能保证原子性。
  • 原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。
  • 例如,i++ 这样的操作,即使 ivolatile 修饰,仍然不是原子操作,因为它包含三个步骤:读取 i 的值、将 i 的值加 1、将结果写回 i。在多线程环境下,这三个步骤可能会被其他线程中断,导致结果错误。

3. 之前 32 位机器上共享的 longdouble 变量的为什么要用 volatile?现在 64 位机器上是否也要设置呢?

  • 在 32 位 JVM 中,longdouble 类型的变量的写操作可能会被拆分成两个 32 位的操作,这就导致在多线程环境下,可能会出现一个线程只读取到部分修改的情况。
  • volatile 可以保证这些变量的写操作是原子性的。
  • 在 64 位 JVM 中,longdouble 类型的写操作通常是原子性的,但为了保证跨平台的兼容性和代码的清晰性,仍然建议在多线程环境下使用 volatile 修饰这些变量。

4. i++ 为什么不能保证原子性?

  • i++ 操作不是原子性的,因为它包含以下三个步骤:
    1. 读取 i 的值。
    2. i 的值加 1。
    3. 将结果写回 i
  • 在多线程环境下,这三个步骤可能会被其他线程中断,导致以下情况:
    • 线程 A 读取 i 的值。
    • 线程 B 读取 i 的值。
    • 线程 A 将 i 的值加 1 并写回。
    • 线程 B 将 i 的值加 1 并写回。
    • 最终,i 的值只被加了 1,而不是 2。

5. volatile 是如何实现可见性的?内存屏障。

  • volatile 通过内存屏障来实现可见性。
  • 当一个线程写入 volatile 变量时,JVM 会在该写操作后插入一个写内存屏障,强制将修改后的值立即写入主内存。
  • 当另一个线程读取 volatile 变量时,JVM 会在该读操作前插入一个读内存屏障,强制其从主内存中读取最新值。

6. volatile 是如何实现有序性的? happens-before 等

  • volatile 通过内存屏障和 happens-before 规则来实现有序性。
  • 内存屏障会禁止指令重排序,保证特定的操作顺序。
  • happens-before 规则定义了内存操作的可见性顺序,保证在多线程环境下,一个操作的结果对另一个操作是可见的。
  • volatile 关键字的 happens-before 规则是:对一个 volatile 变量的写操作 happens-before 后续对这个 volatile 变量的读操作。

7. 说下 volatile 的应用场景?

  • 状态标志:
    • 用于标记某个状态是否发生变化,例如,控制线程是否退出的标志。
  • 单例模式(DCL):
    • 在双重检查锁定(DCL)的单例模式中,使用 volatile 修饰单例对象,防止指令重排序导致的问题。
  • 发布-订阅模式:
    • 用于在发布者和订阅者之间传递数据,保证数据的可见性。
  • 计数器:
    • 在不要求原子性的情况下,可以使用volatile修饰计数器。