Java设计模式之创建型模式 | 单例模式

89 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

本文打📌的方法是推荐用法

0. 前言🎇

大家在网上可以随处可见单例模式的说明,但是我为什么还要写一下这篇文章呢,因为我想把单例模式说得更透彻一些,加上一些考题,让大家知道在源码级别,框架里面哪里使用的单例模式,并且是下单例模式种的细节,我也想一并给大家解释清楚。

1. 单例模式定义

单例模式在设计模式中是一个比较简单的设计模式,它确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

image.png

2. 单例模式使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式

  • 要求生成唯一序列号的场景
  • 在整个项目中需要一个共享访问点和共享数据,例如一个Web页面上的计数器,可以不用每次刷新都记录到数据库中,使用单例模式保持计数器的值,并且确保是线程安全的
  • 创建一个对象需要消耗的资源过多,如需要访问IO数据和数据库等资源
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)

3. 单例模式8种实现方法

3.1 饿汉式(静态常量)


public class Singleton1 {
    public static void main(String[] args) {
        //测试
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2); // true
        System.out.println(instance1.hashCode() == instance2.hashCode());//true
    }
}

class Singleton{
    //1 构造器私有化
    private Singleton(){}
    
    //2 在本类内部创建对象实例
    private final static Singleton instance = new Singleton();

    //3 提供一个公有的静态方法 返回实例对象

    public static Singleton getInstance() {
        return instance;
    }
}

image.png

优缺点说明

优点: 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题

缺点: 在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果

结论:这种单例模式可用,可能造成内存浪费

3.2 饿汉式(静态代码块)

public class Singleton1 {
    public static void main(String[] args) {
        //测试
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2); // true
        System.out.println(instance1.hashCode() == instance2.hashCode());//true
    }
}

class Singleton2{
    //1 构造器私有化
    private Singleton2(){}

    //2 在本类内部创建对象实例
    private static final Singleton2 instance;

    static { //在静态代码块中 创建单例对象
        instance = new Singleton2();
    }

    //3 提供一个公有的静态方法 返回实例对象

    public static Singleton getInstance() {
        return instance;
    }
}

image.png

优缺点同上

3.3 懒汉式(线程不安全)

public class SingletonTest03 {
    public static void main(String[] args) {
        //测试
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2); // true
        System.out.println(instance1.hashCode() == instance2.hashCode());//true

    }
}

class Singleton {
    private static Singleton instanse;

    private Singleton(){};

    //提供一个静态公有方法 当使用到该方法时 才去创建
    //懒汉式

    public static Singleton getInstance() {
        if(instanse == null){
            instanse = new Singleton();
        }
        return instanse;
    }
}

image.png

优缺点说明

优点: 起到了lazy loading的效果,但是只能在单线程下使用

缺点: 如果在多线程下,一个线程进入了if(singleton==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式

结论: 在实际开发中不要使用这种方法

3.4 懒汉式(线程安全)

class Singleton {
    private static Singleton instanse;

    private Singleton(){};

    //提供一个静态公有方法 加入了同步处理的代码 解决线程安全问题
    //懒汉式

    public static synchronized Singleton getInstance() {
        if(instanse == null){
            instanse = new Singleton();
        }
        return instanse;
    }
}

优缺点说明

优点: 解决了线程不安全问题

缺点: 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低

结论: 在实际开发中,不推荐使用这种方式

3.5 懒汉式(同步代码块)

class Singleton {
    private static Singleton instanse;

    private Singleton(){};

    //提供一个静态公有方法 加入了同步处理的代码 解决线程安全问题
    //懒汉式

    public static Singleton getInstance() {
        if(instanse == null){
            synchronized() {
                instanse = new Singleton();
            }
        }
        return instanse;
    }
}

优缺点说明:

这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的的代码块

但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例

结论:在实际开发中,不能使用这种方式

3.6 双重检查(DoubleCheck)📌

class Singleton {
    private static volatile Singleton instanse;

    private Singleton(){};

    //提供一个静态公有方法 加入了双重检查代码 同时解决懒加载问题 解决线程安全问题
    //同时保证效率 推荐使用

    public static Singleton getInstance() {
        if(instanse == null){
            synchronized (Singleton.class) {
                if(instanse == null) {
                    instanse = new Singleton();
                }
            }
        }
        return instanse;
    }
}

推荐原因

Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。

这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.

线程安全;延迟加载;效率较高

结论:在实际开发中,推荐使用这种单例设计模式

3.7 静态内部类📌

class Singleton {
    private static Singleton instanse;

    //构造器私有化
    private Singleton(){};

    //写一个静态内部类 该类中有个静态属性Singleton
    private static class SingletonInstance{
        private static final Singleton Instance = new Singleton();
    }

    //提供一个静态公有方法 直接返回SingletonInstance.Instance

    public static Singleton getInstance() {
        return SingletonInstance.Instance;
    }
}

有关用静态内部类为何是线程安全的会在文末说明

推荐原因

这种方式采用了类装载的机制来保证初始化实例时只有一个线程。

静恣内部类方式在singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点: 避免了线程不安全,利用静态内部类特点实现延迟加载,效率高

结论:推荐使用.

3.8 枚举📌

public class SingletonTest {
    public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
        System.out.println(instance == instance1);//true

    }
}
//使用枚举方式实现单例模式
enum Singleton {
    INSTANCE;//属性
    public void sayOk(){
        System.out.println("ok~");
    }
}

推荐原因

这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象

这种方式是Effective Java作者Josh Bloch提倡的方式

结论:推荐使用

4. 单例模式的优点

  1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  2. 由于单例模式只生成一个实例,所以减少了系统的性能开销,当个对象的产生需要比 较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生 一个单例对象, 然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要 注意JVM垃圾回收机制)。
  3. 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

5. 单例模式的缺点

  1. 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径。它为什么不能增加接口呢? 因为接口对单例模式是没有任何意义的。当然,在特殊情况下,单利模式可以实现接口,被继承等,需要在特点环境下考虑。
  2. 单例模式对测试是不利的。在并行开发环环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式许你一个对象。
  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心他是否是单例,是不是单例要取决于环境。单例模式把“要单例”和业务逻辑混合在了一起。

6. 问题补充

6.1 为什么静态内部类可以保证线程安全

class Singleton {
    private static Singleton instanse;

    //构造器私有化
    private Singleton(){};

    //写一个静态内部类 该类中有个静态属性Singleton
    private static class SingletonInstance{
        private static final Singleton Instance = new Singleton();
    }

    //提供一个静态公有方法 直接返回SingletonInstance.Instance

    public static Singleton getInstance() {
        return SingletonInstance.Instance;
    }
}

静态内部类的优点是:当外部类被加载时,内部类不会被加载,则不会去初始化单例对象,因此不占内存,只有当第一次被调用时,才会导致虚拟机加载内部类。

那么他是如何实现线程安全的呢?

首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法

<clinit>方法: 这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的类变量的赋值动作和静态代码块组成的。JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>()方法。也就是说同一个加载器下,一个类型只会初始化一次

那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>代码,当我们执行getInstance方法的时候,会导致SinglenInstance类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以他只会有一个实例。

假设有一个类A,那么什么时候A会被初始化?

  • T 是一个类,而且一个 T 类型的实例被创建;
  • T 是一个类,且 T 中声明的一个静态方法被调用;
  • T 中声明的一个静态字段被赋值;
  • T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段;
  • T 是一个顶级类(top level class,见 java 语言规范的§7.6),而且一个断言语句嵌套在 T 内部被执行。

6.2 为什么枚举可以实现线程安全

  1. 枚举类型T不可被继承
  2. T中所有属性都被 static final 修饰,天然支持多线程,原因如下

static 类型的属性会在类加载过程初始化, 当一个 Java 类第一次被真正使用到的时候静态资源被初始化、 Java 类的加载和初始化过程都是线程安全的( 因为虚拟机在加载枚举的类的时候, 会使用 ClassLoader 的 loadClass 方法, 而这个方法使用同步代码块保证了线程安全) 。 所以, 创建一个 enum 类型是线程安全的。

6.3 clone()问题

对象的复制情况我们也需要考虑到,在Java中,对象默认是不可以复制的,只有实现了Cloneable接口并且重写了clone方法才可以直接通过对象创建一个新对象。对象复制不依赖于构造函数,因此即使是私有的构造函数,对象仍然可以被复制。因此解决该问题的最好方法就是单例类不要实现Cloneable接口。

7. 面试问题

  • 系统环境信息(System.getProperties())?
  • Spring中怎么保持组件单例的?
  • ServletContext是什么(封装Servlet的信息)?是单例吗?怎么保证?
  • ApplicationContext是什么?是单例吗?怎么保证?
    • ApplicationContext: tomcat:一个应用(部署的一个war包)会有一个应用上下文
    • ApplicationContext: Spring:表示整个IOC容器(怎么保证单例的)。ioc容器中有很多组件(怎么保证单例)
  • 数据库连接池一般怎么创建出来的,怎么保证单实例?

如果小伙伴们感兴趣,我会发文章讲解这些问题!!!

觉得对你有帮助,点个赞再走吧!