当前位置: 首页 > 图灵资讯 > 技术篇> 23种设计模式——单例模式

23种设计模式——单例模式

来源:图灵教育
时间:2023-12-03 17:57:57

今天,我们将学习23种设计模式中的第一种,单例模式,也是设计模式中最简单的模式之一。

概念:

所谓单例模式,就是在整个软件系统中采取一定的方法来保证某一类只能存在对象实例,而且这种类型只提供了一种方法(静态方法)来获取其对象的实例。

特点:
  • 单例类只有一个实例对象;
  • 单例对象必须由单例类创建;
  • 单例类为访问单例类提供全局访问点。
类图分析:

image.png

实现单例模式的八种方式:

1.饿汉式(静态常量)

代码实现:

package cn.ppdxzz.singleton.method01;/** * Description:第一:饿汉式(静态常量) * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println(instance1 == instance2);//输出为true    }}///关键class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在本类内部创建一个对象实例    private final static Singleton instance = new Singleton();    //3.提供公共静态方法(getInstance),返回instance实例对象    public static Singleton getInstance() {        return instance;    }}

描述:

  • 这种写法相对简单,即在类装载时完成实例化,避免线程同步问题。

  • 实例化是在类装载过程中完成的,没有达到Lazy Loading效果。如果这个例子从头到尾都没用过,会造成内存的浪费。

  • 它基于 classloader 然而,机制避免了多线程的同步问题,instance 类装载时实例化,虽然造成类装载的原因有很多,但大部分都是单例模式调用 getInstance 方法, 但也不能确定是否有其他方法(或其它静态方法)导致类装载,此时初始化 instance 显然没有达到 lazy loading 的效果。

  • 结论:这种单例模式是可用的,但可能会造成内存浪费。

2.饿汉式(静态码块)

代码实现:

package cn.ppdxzz.singleton.method02;/** * Description:第二:饿汉式(静态代码块) * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println(instance1 == instance2);///输出仍然是true    }}///关键class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在这类内部创建一个对象实例    private static Singleton instance;    static {///静态代码块在类加载时执行,创建单例对象        instance = new Singleton();    }    //3.提供公共静态方法(getInstance),返回instance实例对象    public static Singleton getInstance() {        return instance;    }}

描述:

  • 这种方法与饿汉风格(静态常量)相匹配 事实上,方法是相似的,但类实例化的过程被放置在静态代码块中,这也是在类装载时执行静态代码块中的代码和初始化的实例。优点和缺点与上述相同。
  • 结论:这种单例模式是可用的,但可能会造成内存浪费。

3.懒汉风格(线程不安全)

代码实现:

package cn.ppdxzz.singleton.method03;/** * Description:第三种:懒汉式(线程不安全) * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println("instance1.hashCode = " + instance1.hashCode());        System.out.println("instance2.hashCode = " + instance2.hashCode());        System.out.println(instance1 == instance2);///输出仍然是true    }}///关键class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在这类内部创建一个对象实例    private static Singleton instance;    //3.提供静态公共方法,使用此方法时,创建返回instance的实例对象,即懒汉风格,用时才创建    public static Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}

描述:

  • 起到了 Lazy Loading 但只能在单线程下使用。
  • 如果一个线程在多线程下进入if (singleton == null)判断句块,在未来实施之前,另一个新的线程也通过了这个判断句,然后会产生多个例子,违反单个例子模式。因此,这种方法不能用于多线程环境 。
  • 结论: 在实际开发中,避免使用这种方法 。

4.懒汉风格(线程安全,同步方法)

代码实现:

package cn.ppdxzz.singleton.method04;/** * Description:第四种:懒汉风格(线程安全,同步方法) * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println("instance1.hashCode = " + instance1.hashCode());//instance1.hashCode = 189568618        System.out.println("instance2.hashCode = " + instance2.hashCode());//instance2.hashCode = 189568618        System.out.println(instance1 == instance2);///输出仍然是true    }}class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在这类内部创建一个对象实例    private static Singleton instance;    //3.提供静态公共方法,添加同步处理的代码,解决线程安全问题    public static synchronized Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance;    }}

描述:

  • 解决了线程安全问题。
  • 该方法同步效率过低 。当每个线程都想得到类的例子时,getInstance() 所有的方法都应该同步。事实上,这种方法只执行一个实例代码就足够了。以后,如果您想获得此类实例,请直接return就可以了。
  • 结论:在实际开发中,不建议使用这种方法。

5.懒汉风格(线程安全,同步代码块)

代码实现:

package cn.ppdxzz.singleton.method05;/** * Description:第五种:懒汉风格(线程安全,同步代码块) * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println("instance1.hashCode = " + instance1.hashCode());//instance1.hashCode = 189568618        System.out.println("instance2.hashCode = " + instance2.hashCode());//instance2.hashCode = 189568618        System.out.println(instance1 == instance2);///输出仍然是true    }}class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在这类内部创建一个对象实例    private static Singleton instance;    //3.提供静态公共方法,添加同步处理的代码,解决线程安全问题    public static Singleton getInstance() {        if (instance == null) {            //重点            synchronized (Singleton.class) {                instance = new Singleton();            }        }        return instance;    }}

描述:

  • 这种方法的初衷是改进第四种实现方法(懒汉风格(线程安全、同步方法),因为之前的同步方法效率太低, 实例化代码块改为同步生成。
  • 但这种同步模式不能起到线程同步的作用。与第三种实现模式遇到的情况一起 如果一个线程进入if (singleton == null)判断句块,还没来得及落实, 另外一个线程也通过了这个判断句,然后就会产生多个例子。
  • 结论:在实际开发中不能使用这种方法 。

6.双重检查

代码实现:

package cn.ppdxzz.singleton.method06;/** * Description:六是双重检查 * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println("instance1.hashCode = " + instance1.hashCode());//instance1.hashCode = 189568618        System.out.println("instance2.hashCode = " + instance2.hashCode());//instance2.hashCode = 189568618        System.out.println(instance1 == instance2);///输出仍然是true    }}class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.在这类内部创建一个对象实例    private static Singleton instance;    //3.双重检查,instance两次判断 == null    public static Singleton getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                if (instance == null) {                    instance = new Singleton();                }            }        }        return instance;    }}

描述:

  • 在多线程开发中经常使用双重检查,如代码所示,我们进行了两次if (singleton == null)检查,确保线程安全。
  • 这样,实例代码只需执行一次,然后再次访问时,判断if (singleton == null),直接 return 实例对象,也避免了方法同步的重复。
  • 线程安全;延迟加载;效率高。
  • 结论:在实际开发中,建议使用这种单例设计模式

7.静态内部类

代码实现:

package cn.ppdxzz.singleton.method07;/** * Description:第七类:静态内部类 * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        //试验用例        Singleton instance1 = Singleton.getInstance();        Singleton instance2 = Singleton.getInstance();        System.out.println("instance1.hashCode = " + instance1.hashCode());//instance1.hashCode = 793589513        System.out.println("instance2.hashCode = " + instance2.hashCode());//instance2.hashCode = 793589513        System.out.println(instance1 == instance2);//输出仍然是true    }}class Singleton {    //1.构造器私有化(防止instance对象直接new)    private Singleton() {}    //2.静态内部类,这一类有静态属性Singleton    private static class SingletonInstance {        private static final Singleton INSTANCE = new Singleton();    }    //3.提供公共静态方法,用于返回INSTANCE实例    public static Singleton getInstance() {        return SingletonInstance.INSTANCE;    }}

描述:

  • 这种方法采用类装载机制,保证初始化实例只有一个线程。
  • 当Singleton类被装载时,静态内部类模式不会立即实例化,而是在需要实例化时,调用getinstance方法,才能装载Singletonstance类,从而完成Singleton 实例化。
  • 类的静态属性只会在第一次加载时初始化,所以在这里,JVM帮助我们确保线程的安全,其他线程在类初始化时无法进入。
  • 避免线程不安全,利用静态内部特性实现延迟加载,效率高。
  • 结论:在实际开发中,建议使用这种单例设计模式

8.枚举

代码实现:

package cn.ppdxzz.singleton.method08;/** * Description:第八种:枚举 * @Author: PeiChen */public class Singleton01 {    public static void main(String[] args) {        ///测试用例        Singleton instance1 = Singleton.INSTANCE;        Singleton instance2 = Singleton.INSTANCE;        System.out.println("instance1.hashCode = " + instance1.hashCode());//instance1.hashCode = 189568618        System.out.println("instance2.hashCode = " + instance2.hashCode());//instance1.hashCode = 189568618        System.out.println(instance1 == instance2);///输出仍然是true    }}enum Singleton {    INSTANCE;    public void method() {    }}

描述:

  • 这种实现还没有被广泛使用,但这是实现单例模式的最佳方式。它更简单,自动支持序列化机制,绝对防止多次实例化。
  • 这种方式是 Effective Java 作者 Josh Bloch 它不仅可以避免多线程同步问题,还可以自动支持序列化机制,防止反序列化重建新对象,绝对防止多次实例化。
  • 不过,由于 JDK1.5 之后才加入 enum 特点,用这种方式写作不可避免地让人感到陌生,在实际工作中,也很少使用。
  • 结论:在实际开发中,建议使用这种单例设计模式
JDK源码分析:

JDK中,java.lang.Runtime是经典的单例模式(饿汉式)

image.png

单例模式总结:
  • 单例模式保证了系统内存中只有一个对象,节省了系统资源。对于一些需要频繁创建和销毁的对象,使用单例模式可以提高系统性能。
  • 要实例化一个单例类,必须记住使用相应的方法来获取对象,而不是使用new关键字。
  • 单例模式使用的场景:需要频繁创建和销毁的对象,需要太多的时间或资源(即重量级对象),但经常使用的对象、工具对象、频繁访问数据库或文件的对象(如数据源、session工厂等)。).

本文总结了今天的单例模式,下一期是第二种设计模式——工厂方法模式,敬请期待。