深刻理解单例设计模式

1、概述

单例模式是面试中常常会被问到的一个问题,网上有大量的文章介绍单例模式的实现,本文也是参考那些优秀的文章来作一个总结,经过本身在学习过程当中的理解进行记录,并补充完善一些内容,一方面巩固本身所学的内容,另外一方面但愿能对其余同窗提供一些帮助。html

本文主要从如下几个方面介绍单例模式:java

  1. 单例模式是什么
  2. 单例模式的使用场景
  3. 单例模式的优缺点
  4. 单例模式的实现(重点)
  5. 总结

2、单例模式是什么

23 种设计模式能够分为三大类:建立型模式、行为型模式、结构型模式。单例模式属于建立型模式的一种,单例模式是最简单的设计模式之一:单例模式只涉及一个类,确保在系统中一个类只有一个实例,并提供一个全局访问入口。许多时候整个系统只须要拥有一个全局对象,这样有利于咱们协调系统总体的行为。面试

3、单例模式的使用场景

一、 日志类spring

日志类一般做为单例实现,并在全部应用程序组件中提供全局日志访问点,而无需在每次执行日志操做时建立对象。数据库

二、 配置类设计模式

将配置类设计为单例实现,好比在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,而后服务进程中的其余对象再经过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。安全

三、工厂类服务器

假设咱们设计了一个带有工厂的应用程序,以在多线程环境中生成带有 ID 的新对象(Acount、Customer、Site、Address 对象)。若是工厂在 2 个不一样的线程中被实例化两次,那么 2 个不一样的对象可能有 2 个重叠的 id。若是咱们将工厂实现为单例,咱们就能够避免这个问题,结合抽象工厂或工厂方法和单例设计模式是一种常见的作法。多线程

四、以共享模式访问资源的类性能

好比网站的计数器,通常也是采用单例模式实现,若是你存在多个计数器,每个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。可是若是采用单例模式实现就不会存在这样的问题,并且还能够避免线程安全问题。

五、在Spring中建立的Bean实例默认都是单例模式存在的。

适用场景:

  • 须要生成惟一序列的环境
  • 须要频繁实例化而后销毁的对象。
  • 建立对象时耗时过多或者耗资源过多,但又常常用到的对象。
  • 方便资源相互通讯的环境

4、单例模式的优缺点

优势:

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的建立销毁对象,减轻 GC 工做,同时能够提升性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点:

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库链接池对象设计为的单例类,可能会致使共享链接池对象的程序过多而出现链接池溢出;
  • 若是实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会致使对象状态的丢失;

5、单例模式的实现(重点)

实现单例模式的步骤以下:

  1. 私有化构造方法,避免外部类经过 new 建立对象
  2. 定义一个私有的静态变量持有本身的类型
  3. 对外提供一个静态的公共方法来获取实例
  4. 若是实现了序列化接口须要保证反序列化不会从新建立对象

一、饿汉式,线程安全

饿汉式单例模式,顾名思义,类一加载就建立对象,这种方式比较经常使用,但容易产生垃圾对象,浪费内存空间。

优势:线程安全,没有加锁,执行效率较高
缺点:不是懒加载,类加载时就初始化,浪费内存空间

懒加载 (lazy loading):使用的时候再建立对象

饿汉式单例是如何保证线程安全的呢?它是基于类加载机制避免了多线程的同步问题,可是若是类被不一样的类加载器加载就会建立不一样的实例。

代码实现,以及使用反射破坏单例:

/**
 * 饿汉式单例测试
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 一、私有化构造方法
    private Singleton(){}
    // 二、定义一个静态变量指向本身类型
    private final static Singleton instance = new Singleton();
    // 三、对外提供一个公共的方法获取实例
    public static Singleton getInstance() {
        return instance;
    }

}

使用反射破坏单例,代码以下:

public class Test {

    public static void main(String[] args) throws Exception{
        // 使用反射破坏单例
        // 获取空参构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 建立实例
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射建立的实例" + singleton);
        System.out.println("正常建立的实例" + Singleton.getInstance());
        System.out.println("正常建立的实例" + Singleton.getInstance());
    }
}

输出结果以下:

反射建立的实例com.example.spring.demo.single.Singleton@6267c3bb
正常建立的实例com.example.spring.demo.single.Singleton@533ddba
正常建立的实例com.example.spring.demo.single.Singleton@533ddba

二、懒汉式,线程不安全

这种方式在单线程下使用没有问题,对于多线程是没法保证单例的,这里列出来是为了和后面使用锁保证线程安全的单例作对比。

优势:懒加载

缺点:线程不安全

代码实现以下:

/**
 * 懒汉式单例,线程不安全
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 一、私有化构造方法
    private Singleton(){ }
    // 二、定义一个静态变量指向本身类型
    private static Singleton instance;
    // 三、对外提供一个公共的方法获取实例
    public static Singleton getInstance() {
        // 判断为 null 的时候再建立对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

使用多线程破坏单例,测试代码以下:

public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("多线程建立的单例:" + Singleton.getInstance());
            }).start();
        }
    }
}

输出结果以下:

多线程建立的单例:com.example.spring.demo.single.Singleton@18396bd5
多线程建立的单例:com.example.spring.demo.single.Singleton@7f23db98
多线程建立的单例:com.example.spring.demo.single.Singleton@5000d44

三、懒汉式,线程安全

懒汉式单例如何保证线程安全呢?经过 synchronized 关键字加锁保证线程安全,synchronized 能够添加在方法上面,也能够添加在代码块上面,这里演示添加在方法上面,存在的问题是每一次调用 getInstance 获取实例时都须要加锁和释放锁,这样是很是影响性能的。

优势:懒加载,线程安全

缺点:效率较低

代码实现以下:

/**
 * 懒汉式单例,方法上面添加 synchronized 保证线程安全
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 一、私有化构造方法
    private Singleton(){ }
    // 二、定义一个静态变量指向本身类型
    private static Singleton instance;
    // 三、对外提供一个公共的方法获取实例
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

四、双重检查锁(DCL, 即 double-checked locking)

实现代码以下:

/**
 * 双重检查锁(DCL, 即 double-checked locking)
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 一、私有化构造方法
    private Singleton() {
    }

    // 二、定义一个静态变量指向本身类型
    private volatile static Singleton instance;

    // 三、对外提供一个公共的方法获取实例
    public synchronized static Singleton getInstance() {
        // 第一重检查是否为 null
        if (instance == null) {
            // 使用 synchronized 加锁
            synchronized (Singleton.class) {
                // 第二重检查是否为 null
                if (instance == null) {
                    // new 关键字建立对象不是原子操做
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优势:懒加载,线程安全,效率较高

缺点:实现较复杂

这里的双重检查是指两次非空判断,锁指的是 synchronized 加锁,为何要进行双重判断,其实很简单,第一重判断,若是实例已经存在,那么就再也不须要进行同步操做,而是直接返回这个实例,若是没有建立,才会进入同步块,同步块的目的与以前相同,目的是为了防止有多个线程同时调用时,致使生成多个实例,有了同步块,每次只能有一个线程调用访问同步块内容,当第一个抢到锁的调用获取了实例以后,这个实例就会被建立,以后的全部调用都不会进入同步块,直接在第一重判断就返回了单例。

关于内部的第二重空判断的做用,当多个线程一块儿到达锁位置时,进行锁竞争,其中一个线程获取锁,若是是第一次进入则为 null,会进行单例对象的建立,完成后释放锁,其余线程获取锁后就会被空判断拦截,直接返回已建立的单例对象。

其中最关键的一个点就是 volatile 关键字的使用,关于 volatile 的详细介绍能够直接搜索 volatile 关键字便可,有不少写的很是好的文章,这里不作详细介绍,简单说明一下,双重检查锁中使用 volatile 的两个重要特性:可见性、禁止指令重排序

这里为何要使用 volatile

这是由于 new 关键字建立对象不是原子操做,建立一个对象会经历下面的步骤:

  1. 在堆内存开辟内存空间
  2. 调用构造方法,初始化对象
  3. 引用变量指向堆内存空间

对应字节码指令以下:

image.png

为了提升性能,编译器和处理器经常会对既定的代码执行顺序进行指令重排序,从源码到最终执行指令会经历以下流程:

graph LR
A[源码] -->B([编译器优化重排序])-->C([指令级并行重排序])-->D([内存系统重排序])-->E[最终执行指令序列]

因此通过指令重排序以后,建立对象的执行顺序可能为 1 2 3 或者 1 3 2 ,所以当某个线程在乱序运行 1 3 2 指令的时候,引用变量指向堆内存空间,这个对象不为 null,可是没有初始化,其余线程有可能这个时候进入了 getInstance 的第一个 if(instance == null) 判断不为 nulll ,致使错误使用了没有初始化的非 null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。

当咱们在引用变量上面添加 volatile 关键字之后,会经过在建立对象指令的先后添加内存屏障来禁止指令重排序,就能够避免这个问题,并且对 volatile 修饰的变量的修改对其余任何线程都是可见的。

五、静态内部类

代码实现以下:

/**
 * 静态内部类实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 一、私有化构造方法
    private Singleton() {
    }
    
    // 二、对外提供获取实例的公共方法
    public static Singleton getInstance() {
        return InnerClass.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerClass{
        private final static Singleton INSTANCE = new Singleton();
    }

}

优势:懒加载,线程安全,效率较高,实现简单

静态内部类单例是如何实现懒加载的呢?首先,咱们先了解下类的加载时机。

虚拟机规范要求有且只有5种状况必须当即对类进行初始化(加载、验证、准备须要在此以前开始):

  1. 遇到 newgetstaticputstaticinvokestatic 这4条字节码指令时。生成这4条指令最多见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(final修饰除外,被final修饰的静态字段是常量,已在编译期把结果放入常量池)的时候,以及调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,若是一个 java.lang.invoke.MethodHandle 实例最后的解析结果是REF_getStaticREF_putStaticREF_invokeStatic 的方法句柄,则须要先触发这个方法句柄所对应的类的初始化。

这5种状况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是 "有且仅有",那么,除此以外的全部引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的状况。

当getInstance()方法被调用时,InnerClass 才在 Singleton 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 INSTANCE 也真正被建立,而后再被 getInstance()方法返回出去,这点同饿汉模式。

那么 INSTANCE 在建立过程当中又是如何保证线程安全的呢?在《深刻理解JAVA虚拟机》中,有这么一句话:

 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,若是多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其余线程都须要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。若是在一个类的 <clinit>() 方法中有耗时很长的操做,就可能形成多个进程阻塞(须要注意的是,其余线程虽然会被阻塞,但若是执行<clinit>()方法后,其余线程唤醒以后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞每每是很隐蔽的。

从上面的分析能够看出INSTANCE在建立过程当中是线程安全的,因此说静态内部类形式的单例可保证线程安全,也能保证单例的惟一性,同时也延迟了单例的实例化。

六、枚举单例

代码实现以下:

/**
 * 枚举实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public enum Singleton {
    INSTANCE;
    public void doSomething(String str) {
        System.out.println(str);
    }
}

优势:简单,高效,线程安全,能够避免经过反射破坏枚举单例

枚举在java中与普通类同样,都能拥有字段与方法,并且枚举实例建立是线程安全的,在任何状况下,它都是一个单例,能够直接经过以下方式调用获取实例:

Singleton singleton = Singleton.INSTANCE;

使用下面的命令反编译枚举类

javap Singleton.class

获得以下内容

Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
  public static final com.spring.demo.singleton.Singleton INSTANCE;
  public static com.spring.demo.singleton.Singleton[] values();
  public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
  public void doSomething(java.lang.String);
  static {};
}

从枚举的反编译结果能够看到,INSTANCE 被 static final 修饰,因此能够经过类名直接调用,而且建立对象的实例是在静态代码块中建立的,由于 static 类型的属性会在类被加载以后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,因此建立一个enum类型是线程安全的。

经过反射破坏枚举,实现代码以下:

public class Test {
    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething("hello enum");

        // 尝试使用反射破坏单例
        // 枚举类没有空参构造方法,反编译后能够看到枚举有一个两个参数的构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
        // 设置强制访问
        declaredConstructor.setAccessible(true);
        // 建立实例,这里会报错,由于没法经过反射建立枚举的实例
        Singleton enumSingleton = declaredConstructor.newInstance();
        System.out.println(enumSingleton);
    }
}

运行结果报以下错误:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at com.spring.demo.singleton.Test.main(Test.java:24)

查看反射建立实例的 newInstance() 方法,有以下判断:

image.png

因此没法经过反射建立枚举的实例。

6、总结

在java中,若是一个Singleton类实现了java.io.Serializable接口,当这个singleton被屡次序列化而后反序列化时,就会建立多个Singleton类的实例。为了不这种状况,应该实现 readResolve 方法。请参阅 javadocs 中的 Serializable () 和 readResolve Method () 。

public class Singleton implements Serializable {
    // 一、私有化构造方法
    private Singleton() {
    }

    // 二、对外提供获取实例的公共方法
    public static Singleton getInstance() {
        return InnerClass.instance;
    }

    // 定义静态内部类
    private static class InnerClass{
        private final static Singleton instance = new Singleton();
    }


    // 对象被反序列化以后,这个方法当即被调用,咱们重写这个方法返回单例对象.
    protected Object readResolve() {
            return getInstance();
    }
}

使用单例设计模式须要注意的点:

  • 多线程- 在多线程应用程序中必须使用单例时,应特别当心。
  • 序列化- 当单例实现 Serializable 接口时,他们必须实现 readResolve 方法以免有 2 个不一样的对象
  • 类加载器- 若是 Singleton 类由 2 个不一样的类加载器加载,咱们将有 2 个不一样的类,每一个类加载一个。
  • 由类名表示的全局访问点- 使用类名获取单例实例。这是一种访问它的简单方法,但它不是很灵活。若是咱们须要替换Sigleton类,代码中的全部引用都应该相应地改变。

本文简单介绍了单例设计模式的几种实现方式,除了枚举单例,其余的全部实现均可以经过反射破坏单例模式,在《effective java》中推荐枚举实现单例模式,在实际场景中使用哪种单例实现,须要根据本身的状况选择,适合当前场景的才是比较好的方式。

参考文章

https://blog.csdn.net/mnb6548...

https://www.oodesign.com/sing...

相关文章
相关标签/搜索