Java多线程、锁、CPU指令重排之单例模式

在面试中有没有被问题到设计模式中的单例模式?有木有在写完一个单例之后,发现面试官把问题引到了多线程、锁、指令重排的问题?有木有一种被虐的体无完肤的感脚?今天来说一下JVM的指令重排,此文章参考了《Java并发编程实战》。
Java多线程中的“有序性”指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。在 Java 领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。



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();并不是一个原子操作,这句话会编译成三条CPU指令:

  1. 分配一块内存 M;
  2. 在内存 M 上初始化 Singleton 对象;
  3. 把 M 的地址赋值给 instance 变量。

我们以为JVM会按一、二、三这种顺序来执行,可是经过优化(指令重排)之后变成了:

  1. 分配一块内存 M;
  2. 将 M 的地址赋值给 instance 变量;
  3. 最后在内存 M 上初始化 Singleton 对象。

优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发***空指针异常***。

***解决方案:***通过使用volatile关键字禁止指令重排。



public 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;
    }
}

你可能感兴趣的:(基础编程,java,多线程,jvm,设计模式)