双重检查锁模式导致空指针异常——JVM指令重排简述

139 阅读3分钟

双重检查锁模式为何会导致空指针异常及解决方案

单例模式的双重检查模式的一般实现如下。

/**
 * 双重检查方式
 */
public class Singleton { 

    //私有构造方法
    private Singleton() {}
    private static Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种实现方式在多线程高并发下可能会出现空指针异常,根本原因是JVM在编译或执行字节码时会优化进行指令重排

如果构造函数中有很多操作,JVM可能会在实例对象所有属性未完全初始化时将对象返回。在对线程中可能导致其他线程获取的并不是完整的实例化对象,此时就会出现空指针异常。

要解决这个问题可以使用 volatile 关键字, volatile遵行happens-before原则,即:在读操作前,写操作必须全部完成。

/**
 * 双重检查方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

下面来详细介绍一下JVM的指令重排

每条指令都可以分为以下五个步骤

  1. 取指令
  2. 指令译码
  3. 执行指令
  4. 内存访问
  5. 数据写回

在不改变程序结果的情况下可以通过重排序来优化执行速度。

例如下面的例子

int a = 1; 
int b = 2; 
a = a + 1; 
b = b + 1; 

(我们先只考虑指令的存取操作)执行这段代码需要一下几条指令:Load a、Set to 1、Store a、Load b、Set to 2、Store b ......

经过指令重排后可以优化为

int a = 1; 
a = a + 1; 
int b = 2; 
b = b + 1; 

优化后可以减少一次Load a、Store a以及Load b、Store b


在单例模式中可能会出现如下情况

  1. 线程A执行getInstance()方法,发现instance为空,进入同步块。
  2. 线程A在同步块中创建instance对象,并赋值给instance变量。
  3. 由于JVM的指令重排优化,instance对象的初始化过程可能被分为三个步骤:(1)分配内存空间;(2)初始化对象;(3)将内存地址赋值给instance变量。但是这三个步骤的执行顺序并不一定按照1-2-3来进行,有可能是1-3-2。
  4. 如果线程A执行了1-3-2的顺序,那么在第三步完成后,instance变量就不再为空了,但是此时对象还没有完全初始化。
  5. 线程B执行getInstance()方法,发现instance不为空,直接返回instance对象。
  6. 线程B使用返回的instance对象调用其方法时,就可能出现空指针异常,因为此时对象还没有被正确初始化。