美文网首页
正确的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 单例双检查

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

  • java设计模式之单例模式

    单例模式属于java设计模式的一种,最常见实现方式有以下几种 懒汉、饿汉、双重检查单例、静态内部类单例。 单例模式...

  • 你真的会写单例吗?

    你真的会写单例吗? 摘录来源 单例的正确姿势 Java单例模式可能是最简单也是最常用的设计模式,一个完美的单例需要...

  • 设计模式详解——单例模式

    本篇文章介绍一种设计模式——单例模式。本文参考文章:《JAVA与模式》之单例模式,如何正确地写出单例模式。 一、单...

  • 老司机来教你单例的正确姿势

    老司机来教你单例的正确姿势 Java单例模式可能是最简单也是最常用的设计模式,一个完美的单例需要做到哪些事呢? 单...

  • 剑指offer4J【C2 P2】 实现懒汉单例

    线程安全懒汉单例一般继续双检查,检查-》加锁-》检查-》构建 源码: 剑指offer4J[https://gith...

  • 2020-11-02-Spring单例 vs. 单例模式

    Spring 单例不是 Java 单例。本文讨论 Spring 的单例与单例模式的区别。 前言 单例是 Sprin...

  • 设计模式笔记

    设计模式再学习链接 一、创建型 01)单例模式(Singleton Pattern) 双null检查. 02)简单...

  • Java中单例模式你用的哪一种?

    一起讨论java中的单例模式。单例模式是java设计模式中算是最简单的设计模式了。 * java实现单例模式的写法...

  • gof23创建类模式(golang版)

    区块链的征程已开启 单例模式 Java中的单例模式的实现可以有饿汉式、懒汉式、双锁、静态内部类、枚举等形式,在go...

网友评论

      本文标题:正确的Java 单例双检查

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