指令重排序可能造成的危害

102 阅读2分钟

指令重排序在多线程环境中会导致数据竞争问题,尤其是在双重检查锁(DCL)单例模式或其他对指令顺序敏感的场景中。我们可以通过一个简单的单例模式例子来说明指令重排序可能带来的数据竞争问题。

示例:双重检查锁(DCL)实现的单例模式

通常,我们会使用双重检查锁(Double-Checked Locking, DCL)来实现线程安全的单例模式,以确保只初始化一次实例,避免多个线程同时创建对象。以下是常见的双重检查锁单例实现:

public 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. 分配内存:给Singleton对象分配内存;
  2. 初始化对象:调用Singleton的构造函数,完成对象初始化;
  3. 设置引用:将instance指向刚刚分配的内存地址。

在没有禁止指令重排序的情况下,这三个步骤可能被重排序,执行顺序可能变为分配内存 → 设置引用 → 初始化对象。这样,在一个线程调用getInstance()时,如果另一个线程也调用了getInstance(),就可能出现以下情况:

线程1和线程2同时执行getInstance()

  1. 线程1进入getInstance()方法,发现instancenull,进入第一个if检查。
  2. 线程1获得锁,开始创建实例。
  3. 线程1instance分配内存,并将instance指向该内存区域,但尚未完成对象初始化。
  4. 线程2进入getInstance()方法,发现instance不再是null(因为线程1已经将其指向内存地址),于是直接返回instance
  5. 线程2使用未完全初始化的instance,导致程序出错。

结果分析

由于指令重排序,instance可能在对象初始化完成之前就被其他线程可见,从而导致其他线程访问一个未初始化的对象。这种情况就是指令重排序带来的数据竞争问题。

解决方案

可以通过将instance声明为volatile变量,防止指令重排序:

private static volatile Singleton instance;

在Java中,volatile关键字可以禁止指令重排序,确保对象初始化的顺序不会改变,保证多线程环境下的可见性和有序性,从而避免数据竞争。