美文网首页
正确的Java 单例双检查

正确的Java 单例双检查

作者: 清风流苏 | 来源:发表于2018-11-27 10:30 被阅读3127次

    说来惭愧,下面的代码,我一直以为是线程安全的,直到昨天使用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 单例双检查

          本文链接:https://www.haomeiwen.com/subject/rdwwqqtx.html