设计模式学习笔记|单例模式 Singleton

时间:2022-07-23
本文章向大家介绍设计模式学习笔记|单例模式 Singleton,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

单例模式:饿汉、懒汉、枚举类!!!!

单例模式是设计模式中比较经常听说的设计模式,也是比较容易掌握的设计模式。基本上接触过设计模式的人别的模式不一定能说出来,但是一般“单例模式”和“工厂模式”是都能说出来的。

很多时候,我们都会以为单例模式是比较好掌握的,但是后来在我的学习当中,我发现还是有很多问题是没有考虑到的,甚至是想象不到的。

单例模式是要使类的实例在内存中只有一份。听起来挺容易的,但是这个还真是没有想象的那么简单。我的代码使用 Java 来进行描述。

通常情况下,在使用 Java 来完成 单例模式 的时候,都知道存在两种写法,一种是饿汉模式,另一种是懒汉模式。所谓饿汉模式,就是在类加载入内存之后,直接实例化一个对象出来;懒汉模式是在需要的时候再去实例化一个对象出来。

为什么有饿汉模式和懒汉模式呢?这得从它们的加载时机来考虑。很多人认为,饿汉模式在类进入内存就实例化一个对象有些不妥,因为没有使用,为什么要着急实例化呢,所以就出现了懒汉模式。懒汉模式是在需要的时候才去实例化类的对象,但是懒汉模式会因为多线程的问题,会导致实例化多个对象出来,而此时就需要解决多线程同步的问题。解决多线程同步的问题,就需要用到锁,那么就又带来了效率上的问题。

说了这么多,那么来看看,到底如何来使用 Java 语言完成一个 单例模式。

饿汉模式

先来看看饿汉模式的代码:

public class Singleton01 {

    private static final Singleton01 INSTANCE = new Singleton01();

    // 构造函数为 private
    private Singleton01() {}

    public static Singleton01 getInstance() { return INSTANCE; }

    // 此处模拟类中处理业务的方法
    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Singleton01 s1 = Singleton01.getInstance();
        Singleton01 s2 = Singleton01.getInstance();

        // 两个引用指向的是一个对象
        System.out.println(s1 == s2);
    }
}

单例模式的第一步就是将 构造方法 的访问修饰符设置为 private,使得外部无法直接实例化。然后在类中定义一个静态的 getInstance 方法用来获取实例。

使用饿汉模式的单例,在类加载到内存后,静态变量只实例化一次,JVM 保证其线程的安全。

其缺点是,不管该类是否要使用,都会马上得到一个实例。因此,这就有了懒汉模式。

懒汉模式

懒汉模式的单例的代码:

public class Singleton06 {
    private static volatile Singleton06 INSTANCE;

    private Singleton06() {}

    public static Singleton06 getInstance() {
        if (INSTANCE == null) {
            // 双重检查
            synchronized (Singleton06.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    INSTANCE = new Singleton06();
                }
            }
        }

        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i ++) {
            new Thread(()->{
                System.out.println(Singleton06.getInstance().hashCode());
            }).start();
        }
    }
}

以上就是懒汉模式的代码。

在懒汉模式中,使用了 synchronized 来解决方法“不可重入”的问题,其中使用 Thread.sleep 来让线程休息一下,从而让出线程所占用的 CPU 而产生线程的切换。可以把第一个 if 判断和 synchronized 两行删掉,只留下最里面的 if 语句块的内容,就会发现会实例化多个对象了。

实例化多个对象

在 Java 中提供了反射的机制,即使使用单例模式,仍然可以实例化出多个对象。无论是上面的饿汉模式,还是懒汉模式,都可以实例化多个实例。

这里使用第一个饿汉模式的代码进行测试,测试代码如下:

Class<?> aClass = Class.();
Singleton01 s3 = (Singleton01) aClass.newInstance();

代码很简单,只有上面两句,但是这样就已经实例化出了一个对象,且通过 s3 可以调用该类中的方法。

因此这样,就可以实例化对象出来了,内存中就有了一个类的多个实例了。

枚举类的单例

枚举在很多语言中都有,一般情况就是定义一些有限的常量。其实,枚举类中可以定义方法。看一下枚举类的单例代码,代码如下:

public enum Singleton08 {
    INSTANCE;

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i ++) {
            new Thread(()->{
                System.out.println(Singleton08.INSTANCE.hashCode());
            }).start();
        }
    }
}

以上代码,仍然通过 new Thread 多线程来得到其实例。

但是,通过输出可以看出,其 hashCode 始终是一样的。

接着使用上面的反射来获取枚举类的实例,代码如下:

Class<?> aClass = ;
{
    aClass = Class.();
    Singleton01 s3 = (Singleton01) aClass.newInstance();

然后代码执行到 newInstance 方法时会报错,提示访问异常。

因为枚举类没有定义构造函数,因此无法实例化。

也就是说使用枚举类,即可以保证线程的安全,也可以防止反射来实例化。算是一种完美的方法。

最后

看似简单的单例模式,其中竟然也蕴含着这么多的知识点,学完真是受益非浅。虽然只是一个单例模式,掌握了一种设计模式,但是从各种实现中,又学到了很多其他的知识。比如,类实例化的时机,多线程方法的不可重入,枚举类的另类用法等。

所以,知识如果能够串联起来,那么才能把学到的知识融会贯通,真正掌握和吸收。

这就是我关于设计模式中单例模式的一篇笔记。