单例模式与DCL双重校验锁

CodeAegis
• 阅读 6896

前言

在面试里面,单例模式是经常被问到的设计模式。今天正好学习完了《Java并发编程实战》,该书的最后一章讲得就是JMM(Java内存模型),其中就提到了以DCL方式实现单例模式的优缺点。

单例模式

单例模式的概念就不在这里赘述了。在保证线程安全的前提下,最简单的实现方式是“饿汉式”,即在加载单例类的字节码时,在初始化阶段对静态的instance变量进行赋值,代码如下。

//“饿汉式”实现线程安全的单例模式
public class Singleton {
    private static Singleton instance = new Singleton();
    
    private Singleton() {
    }
    
    public static Singletion getInstance() {
        return instance;
    }
}

如果我们希望延迟初始化这个单例对象,就不能使用上述的“饿汉式”实现,而要使用“懒汉式”的实现。最容易想到的一种实现方式当然是使用synchronized关键字对getInstance()方法进行修饰。代码如下。

//使用同步方法实现的单例模式
public class Singleton {
    private static Singleton instance;
    
    private Singleton(){
    }
    
    public static synchronized getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        
        return instance;
    }
}

这是最简单的单例模式的延迟初始化实现版本,并且通过synchonized锁住了Singleton这个类的字节码,保证了线程安全。但是,这种锁字节码的方式粒度太大,同一时间只能有一个线程执行同步方法拿到这个单例,因此,在高并发环境下,吞吐量严重受限。

为了提升并发性能,DCL(double checked lock)实现方式看起来是不错的选择。代码如下。

//实现双重校验锁实现单例模式
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;
    }
}

DCL方式将同步方法改成了同步代码块,锁的粒度缩小,并发性能更好。当单例对象已经被创建之后,多个线程可以同时执行第一个if条件判断并且拿到单例对象。当单例对象未被创建时,同一时间只有一个线程能进入同步代码块进行第二次if条件判断,如果发现此时单例对象仍没有被其他线程所创建,则创建单例对象。

DCL方式实现的关键点在于volatile关键字。volatile关键字有两个作用:(1) 保证共享变量在修改之后对于其他线程的可见性;(2) 禁止JVM在执行字节码时发生的指令重排。鄙人认为,volatile关键字在上述代码的真正作用是禁止指令重排而不是保证可见性。为什么这么说呢,因为同步代码块上的synchronized关键字本身就有保证可见性和原子性的作用。因此,如果从可见性的角度而言,大可不必使用volatile。换言之,当代码执行到同步代码块中的第二个if条件判断,如果先前已经有线程创建了对象,由于synchronized关键字能够保证可见性,当前线程的高速缓存中存的instance的值(null)就会失效,导致当前线程一定会到内存中重新读取instance的值进行第二次if判断。因此,volatile的真正作用就是防止在new这个单例对象时发生的指令重排现象,即防止其他线程访问并拿到未完全初始化的单例对象。问题来了,这种情况是如何发生的呢?下面我尝试从字节码的执行过程来进行分析。

我们单看instance = new Singleton()这行代码,其对应的字节码如下:

1 NEW // 在堆内存中分配内存,将指向该区域的引用放入操作数栈
2 DUP // 在操作数栈中复制引用
3 INVOKESPECIAL // 调用Singleton类的构造方法
4 PUTSTATIC // 将引用赋值给静态变量instance

在JVM执行以上字节码的时候,如果不加volatile关键字,那么可能在DUP指令(指令2)执行之后,跳过执行构造方法的指令(指令3),而直接执行PUTSTATIC指令(指令4),然后用操作数栈上剩下的引用来执行指令3。因为在单线程环境下,JVM认为打乱指令3、4的执行顺序并不会影响程序的正确性。但是,在多线程环境下,如果指令3、4发生重排,当执行完指令1、2、4之后,instance对象已经不再为null,此时来一个线程调用getInstance方法,就会拿到一个尚未完全初始化的对象,从而发生对象逃逸。这种现象在单例类的构造函数耗时很大时更加频繁。而volatile关键字的存在则告诉JVM,在处理被volatile修饰的变量时,禁止使用指令重排。

但是,volatile的禁止指令重排功能在Java 5及之后才有作用,因此,DCL的实现方式在早前的版本就不起作用了。根据《Java并发编程实战》的介绍:“DCL使用方法已经被广泛废弃——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及JVM启动很慢)已经不复存在,因为它不是一种高效的优化措施。”

因此,实现单例的更好方式,应该是使用静态内部类的延迟初始化机制。代码如下:

// 通过静态内部类的延迟初始化机制实现单例模式
public class Singleton {
    private Singleton() {
    }
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

上述代码只有在有线程调用getInstance方法时才会完成静态内部类Singleton.SingletonHolder的加载过程(类加载、链接、初始化)。

当然,单例模式还可以通过编写enum类来实现。代码就不写了吧。

点赞
收藏
评论区
推荐文章
3A网络 3A网络
2年前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非
Wesley13 Wesley13
3年前
java 23种设计模式(五、单例模式)
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式的结构  单例模式的特点:单例类只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。  饿汉式单例类publicclassEagerSingleton
Wesley13 Wesley13
3年前
java中饿汉与懒汉的故事(单例设计模式)
java中的单例设计模式关于设计模式,这其实是单独存在的东西,它不属于java,但是在java中使用较多,所以今天我就给大家介绍下单例设计模式中的饿汉和懒汉这俩朴素的打工人。首先我先说明下单例设计模式是啥(如果不想了解,可以直接划下去看饿汉和懒汉):类的单例设计模式就是采用一定的方法保证在整个软件系统中,对某个类只能存在一
Wesley13 Wesley13
3年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
3年前
Java单例模式
什么是单例模式  单例模式是在程序中,一个类保证只有一个实例,并提供统一的访问入口。为什么要用单例模式节省内存节省计算如对象实例中的一样的,那就不用每次都创建一个对象方便管理因为单例提供一个统一的访问入口,不需要创建N多个对象,很多工具类都用了单例实现,如日志、字符串工具类
Wesley13 Wesley13
3年前
PHP单例模式(精讲)
首先我们要明确单例模式这个概念,那么什么是单例模式呢?单例模式顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类我们称之为单例类。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例
Stella981 Stella981
3年前
C#设计模式(1)——单例模式(Singleton)
单例模式即所谓的一个类只能有一个实例,也就是类只能在内部实例一次,然后提供这一实例,外部无法对此类实例化。单例模式的特点:1、只能有一个实例;2、只能自己创建自己的唯一实例;3、必须给所有其他的对象提供这一实例。普通单例模式(没有考虑线程安全)  ///<summary///单例模式
Wesley13 Wesley13
3年前
(面试常问)4种单例设计模式的总结(内含代码以及分析)
单例设计模式:  单例模式,是一种常见的软件设计模式.在它的核心结构中只包含了一个被称为单例的特殊类.通过单例模式可以保证系统中只有该类的一个实例对象.优点:  实例控制:单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问的是唯一的实例   灵活性:因为类控制了实例化过程,所以类可以很灵活的更改实
Wesley13 Wesley13
3年前
Java设计模式:Singleton(单例)模式
概念定义Singleton(单例)模式是指在程序运行期间,某些类只实例化一次,创建一个全局唯一对象。因此,单例类只能有一个实例,且必须自己创建自己的这个唯一实例,并对外提供访问该实例的方式。单例模式主要是为了避免创建多个实例造成的资源浪费,以及多个实例多次调用容易导致结果出现不一致等问题。例如,一个系统只能有一个窗口管理器或文件系统,一个程
Stella981 Stella981
3年前
Python设计模式
对于很多开发人员来说,单例模式算是比较简单常用、也是最早接触的设计模式了,仔细研究起来单例模式似乎又不像看起来那么简单。我们知道单例模式适用于提供全局唯一访问点,频繁需要创建及销毁对象等场合,的确方便了项目开发,但是单例模式本身也有一定的局限性,如果滥用则会给后续软件框架的扩展和维护带来隐患。单例模式的实现有很多种,应用场合也各有不同,但必须保证实例唯一
Wesley13 Wesley13
3年前
Java单例模式实现方式
懒汉式非线程安全publicclassLazyNoSafe{privatestaticLazyNoSafeinstance;publicstaticLazyNoSafegetInstance(){if(instancenull){