正确的Java 单例双检查

说来惭愧,下面的代码,我一直以为是线程安全的,直到昨天使用Jenkins对项目做静态代码分析的时候,发现其将这种写法标为红色醒目的bug。

// 非线程安全版本
public final class Singleton {
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

导致线程不安全的根源出在INSTANCE = new Singleton();这一行上。这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情:

  1. 给INSTANCE 分配内存
  2. 调用Singleton的构造函数初始化成员变量
  3. 将INSTANCE 对象指向分配的内存空间(执行完后INSTANCE 就非null了)
    但是在JVM的即时编译器中存在指令重排序的优化。上面第2步和第3步的执行顺序不能保证。可能Singleton的构造函数初始化还未完成或者未执行,就已将INSTANCE的实例指向了未完全初始化的Singleton对象。在多线程运行中,一个线程正在进行初始化INSTANCE的成员变量,另一个线程可能就已经开始使用其成员变量了,从而导致crash或者其他异常出现。

解决办法,给INSTANCE实例加上volatile关键字。

// 线程安全版本
public final class Singleton {
    private static volatile Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

在这里,volatile关键字的作用是禁止指令重排序。在volatile变量的赋值操作后面有一个内存屏障,读操作不会被重排到内存屏障之前。

注意,Java 5之前的版本使用volatile的双检查还是有问题。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

事实上,以上版本还可以做性能优化提升。

// 性能更好的线程安全版本
public final class Singleton {
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        Singleton temp = INSTANCE;
        if (temp == null) {
            synchronized (Singleton.class) {
                temp = INSTANCE;
                if (temp == null) {
                    INSTANCE = temp = new Singleton();
                }
            }
        }
        return temp;
    }
}

使用中间变量temp来存储INSTANCE,其作用是在INSTANCE字段已经初始化的情况(大部分情况),由volatile修饰的INSTANCE字段只需要读取一次(注意是return temp而不是return INSTANCE)。这种写法,性能可以提升25%。具体可以参见wiki。

正确使用双检查还是挺麻烦的,所以呢,个人推荐使用下面的静态内部类来保证线程安全性。

// 线程安全
public final class Singleton {
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
    private static final class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

参考资料:

  1. https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
  2. http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
  3. http://www.blogjava.net/kenzhh/archive/2016/05/16/357824.html

你可能感兴趣的:(正确的Java 单例双检查)