从零开始学习Java设计模式 | 创建型模式篇:单例设计模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 创建型模式篇:单例设计模式相关的知识,希望对你有一定的参考价值。

从本讲开始,咱们就要开始正式学习23种设计模式了。当然,我们得按照顺序来学,首先先来学习23种设计模式里面的第一类模式,即创建型模式。

创建型模式的主要关注点是"怎样创建对象?",它的主要特点是"将对象的创建与使用分离"。也就是说,如果我们作为一个使用者的话,那么我们可以直接通过某种方式去获取别人创建好的对象,至于别人是如何创建的,我们并不关注。这样,就可以降低系统的耦合度了,使用者并不需要关注对象的创建细节,尤其是那些创建特别麻烦的对象。于是,创建型模式的好处就这样体现出来了。

此外,我们还应知道,创建型模式分为以下几种:

  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式

注意,在本讲中,我们先来学习第一种创建型模式,即单例模式。

概述

单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例设计模式的概念讲完之后,接下来我们再来看一下单例设计模式的结构。

结构

单例设计模式主要有两种角色,它们分别是:

  • 单例类:该类只能创建一个对象
  • 访问类:该类其实就是测试类,即使用单例类

实现

在介绍单例设计模式的实现之前,我们先来看一下单例设计模式的分类,单例设计模式可分为如下两类:

  • 饿汉式:类加载就会导致该单实例对象被创建。也就是说类加载的时候,该类的对象就创建好了
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

所以,我们如何去区分饿汉式和懒汉式呢?只需要去判断该单实例对象是否是在类加载的时候创建即可,若是,则是饿汉式,若不是,则是懒汉式。

饿汉式

在本节,我们先来看一下饿汉式是如何来实现的。大家要注意了,饿汉式又可分为多种实现方式,在这里,我只介绍两种,它们分别是:

  • 静态成员变量方式
  • 静态代码块方式

下面,我们先来看静态成员变量这种实现方式。

实现方式一:静态成员变量

打开咱们的maven工程,并在com.meimeixia包下再创建一个子包,即pattern,在该包下存放的都是我们编写的有关设计模式的代码,然后在pattern包下再创建一个子包,即singleton,这表明编写的有关单例设计模式的代码我们都会放在该包下。

由于单例设计模式有多种实现方式,要想讲清楚每一种实现方式,我们还得在com.meimeixia.pattern.singleton包下分别创建多个子包来存放每一种实现方式所对应的代码,例如com.meimeixia.pattern.singleton.demo1包里面存放的就是饿汉式第一种实现方式(即静态成员变量方式)所对应的代码。

以上工作都做好之后,下面咱们就得来正式编写代码实现饿汉式的第一种实现方式(即静态成员变量方式)了。

首先,在com.meimeixia.pattern.singleton.demo1包下创建一个单例类,不妨我们就命名为Singleton。

那如何创建该类呢?只要大家跟着我的思路一步一步来,相信大家肯定能创建出来。

  1. 私有构造方法:为什么要私有构造方法呢?因为私有了构造方法之后,外界就访问不到这个构造方法了,访问不到的话外界就无法去创建对象了
  2. 在本类中创建一个本类对象供外界去使用
  3. 提供一个公共的访问方式,让外界获取该对象

经过上述三步,相信你很快就能创建出Singleton类了。

package com.meimeixia.pattern.singleton.demo1;

/**
 * 饿汉式:静态成员变量
 * @author liayun
 * @create 2021-05-28 18:07
 */
public class Singleton 

    // 1. 私有构造方法
    private Singleton() 

    // 2. 在本类中创建本类对象
    private static Singleton instance = new Singleton();

    // 3. 提供一个公共的访问方式,让外界获取该对象
    public static Singleton getInstance() 
        return instance;
    


注意,以上该类中对外提供的公共访问方法,除了使用public修饰之外,还使用了static修饰,这是为什么呢?因为外界无法创建Singleton类的对象,既然不能创建对象的话,那么就无法去调用其非静态方法了,所以这里面我们对外要提供的是静态方法。而且,由于静态的不能直接访问非静态的,所以instance成员变量还得使用static来修饰。

然后,创建一个测试类,这里我们不妨就命名为Client。

package com.meimeixia.pattern.singleton.demo1;

/**
 * @author liayun
 * @create 2021-05-28 18:16
 */
public class Client 

    public static void main(String[] args) 
        // 创建Singleton类的对象
        Singleton instance = Singleton.getInstance();

        Singleton instance1 = Singleton.getInstance();

        // 判断获取到的两个是否是同一个对象
        System.out.println(instance == instance1);
    


可以看到,两次获取到Singleton类的对象之后,我们还得再来判断一下获取到的两个对象是不是同一个对象。

此时,我们运行一下以上测试类,如下图所示,可以看到打印结果是true,也就是说两次获取到Singleton类的对象是同一个对象,这样,我们就已经保证Singleton这个类只能创建一个对象了。

实现方式二:静态代码块

饿汉式的第一种实现方式我们已经实现了,接下来,我们再来看一下饿汉式的第二种实现方式,即静态代码块方式。

首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo2,这表明编写的饿汉式的第二种实现方式的代码我们都会放在该包下。

然后,在com.meimeixia.pattern.singleton.demo2包下创建一个单例类,该类我们同样命名为Singleton。

同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。

  1. 私有构造方法:私有构造方法其实就是为了让外界不能创建该类的对象
  2. 在成员位置声明一个该类的成员变量,不过不要给其赋值
  3. 在静态代码块中进行赋值
  4. 对外提供一个公共的访问方式,让外界获取该对象

经过上述四步,相信你很快就能创建出Singleton类了。

package com.meimeixia.pattern.singleton.demo2;

/**
 * 饿汉式:静态代码块
 * @author liayun
 * @create 2021-05-28 18:29
 */
public class Singleton 

    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量
    private static Singleton instance;

    // 在静态代码块中进行赋值
    static 
        instance = new Singleton();
    

    // 对外提供获取该类对象的方法
    public static Singleton getInstance() 
        return instance;
    


你会发现,这和饿汉式的第一种实现方式唯一的区别是:对于饿汉式的第一种实现方式来说,在单例类里面是在成员位置声明了一个该类的成员变量,声明的同时并直接为其进行了赋值;而对于饿汉式的第二种实现方式而言,在单例类里面是先在成员位置声明了一个该类的成员变量,然后再在静态代码块里面对其进行赋值。除此之外,这两种实现方式就差不多了,其他就没有什么区别了。

紧接着,创建一个测试类,同理我们不妨命名为Client。

package com.meimeixia.pattern.singleton.demo2;

/**
 * @author liayun
 * @create 2021-05-28 23:47
 */
public class Client 
    public static void main(String[] args) 
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        // 判断两次获取到的Singleton对象是否是同一个对象
        System.out.println(instance == instance1);
    

最后,我们运行一下以上测试类,若打印结果是true,则说明我们确实已经做到了单例;若打印结果是false,则说明我们写的代码是有问题的。如下图所示,可以看到打印结果是true,也就是说两次获取到Singleton类的对象是同一个对象,当然了,多次获取到的肯定也是同一个对象,这不用说。

至此,我就将饿汉式的两种实现方式讲解完了。那么我们不妨再来总结一下饿汉式有什么特点?不管是饿汉式的第一种实现方式还是第二种实现方式,它们都属于是在类加载的时候就已经创建了该类的对象。

大家不妨设想一下这种情况,如果我们只是对单例类进行了一个类加载的操作,并没有去获取该类的对象,那么这个对象是不是已经存在于内存中了啊!当然了,前提是使用饿汉式的方式来实现单例。此时,如果我们一直不用它的话,那么你会发现它是一直存在于内存中的,这势必就会造成内存的浪费。也就是说,饿汉式的另外一个特点是会造成内存的浪费。

由于饿汉式会浪费内存,所以我们还得去学习一下懒汉式,下面我就会讲到。

懒汉式

饿汉式的两种实现方式讲完之后,接下来我们来看一下懒汉式。当然,懒汉式它也有多种实现方式,下面我会一一介绍给大家。

接下来,我们先来看懒汉式的第一种实现方式,即线程不安全的方式。

实现方式一:线程不安全

首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo3,这表明编写的懒汉式的第一种实现方式的代码我们都会放在该包下。

然后,在com.meimeixia.pattern.singleton.demo3包下创建一个单例类,该类我们同样命名为Singleton。

同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。

  1. 私有构造方法
  2. 在成员位置声明一个该类的成员变量,不过不要给其赋值
  3. 对外提供一个公共的访问方式

经过以上三个步骤,可能有些同学写出来的Singleton类是下面这样的。

package com.meimeixia.pattern.singleton.demo3;

/**
 * 懒汉式
 * @author liayun
 * @create 2021-05-29 17:57
 */
public class Singleton 

    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量instance
    private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值

    // 对外提供访问方式
    public static synchronized Singleton getInstance() 
        instance = new Singleton();
        return instance;
    


写出来之后,他还很得意,他说懒汉式的概念不就是在首次使用单实例对象的时候才创建的嘛,首次使用无非就是调用以上getInstance方法呗,所以我在该方法里面给其赋值并返回出去,不是合情合理的吗?是,懒汉式的概念理解得是没错,但他没意识到一个问题,就是外界每调用一次getInstance方法,都会发现又重新new了一个对象,这样,多次获取到的肯定就不是同一个对象了。

你要是不信的话,不妨编写一个测试类来测试一下。

package com.meimeixia.pattern.singleton.demo3;

/**
 * @author liayun
 * @create 2021-05-29 18:03
 */
public class Client 

    public static void main(String[] args) 
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();

        // 判断两次获取到的Singleton对象是否是同一个对象
        System.out.println(instance == instance1);
    


运行以上测试类之后,如下图所示,你会发现打印结果是false,为什么是false呢?上面我已经分析过了,这里不再赘述。

而我们现在想要的是以上Singleton类只能创建一个对象,所以我们得在Singleton类的getInstance方法里面加上一个如下if判断。

package com.meimeixia.pattern.singleton.demo3;

/**
 * 懒汉式
 * @author liayun
 * @create 2021-05-29 17:57
 */
public class Singleton 

    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量instance
    private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值

    // 对外提供访问方式
    public static Singleton getInstance() 
        // 判断instance是否为null,如果为null,那么说明还没有创建Singleton类的对象
        // 如果没有创建的话,那么我们就创建一个并返回;如果有创建,那么直接返回即可
        if (instance == null) 
            instance = new Singleton();
        
        return instance;
    


代码改进之后,不妨再来运行一下测试类,如下图所示,发现打印结果是true,这说明咱们多次获取到的是同一个对象。

你觉得以上Singleton类写的有没有什么问题啊?肯定是存在问题的,如果是多线程环境下,那么就会出现线程安全问题。

为什么这么说呢?假设现在是多线程环境,两个线程同时调用getInstance方法,线程1拿到cpu的执行权,它在调用getInstance方法时,首先肯定是要做一个判断的,做完判断之后,它会进入到判断里面。此时,如果线程2拿到了cpu的执行权,那么线程1就会处于等待状态,同样,线程2在调用getInstance方法时也是要进行判断的,你觉得线程2还能进入到判断里面吗?

肯定能够啊!因为此时if判断条件是成立的,即instance是等于null的,这一切都是由于线程1还处于等待状态,还未执行下面的代码而导致的。也就是说,只要线程2获取到了cpu的执行权,那么它也会进入到判断里面。这样,以上Singleton类创建的就不是单个对象了,而是多个。

实现方式二:线程安全

既然以上实现方式在多线程环境下会出现线程安全问题,那么我们就得改进代码了,如何进行改进呢?很简单,只须在以上Singleton类的getInstance方法上加上一个同步关键字(即synchronized)即可。

package com.meimeixia.pattern.singleton.demo3;

/**
 * 懒汉式
 * @author liayun
 * @create 2021-05-29 17:57
 */
public class Singleton 

    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量instance
    private static Singleton instance; // 只是声明了一个该类型的变量,并没有对其进行赋值

    // 对外提供访问方式
    public static synchronized Singleton getInstance() 
        // 判断instance是否为null,如果为null,那么说明还没有创建Singleton类的对象
        // 如果没有创建的话,那么我们就创建一个并返回;如果有创建,那么直接返回即可
        if (instance == null) 
            // 线程1等待,线程2获取到cpu的执行权,也会进入到该判断里面
            instance = new Singleton();
        
        return instance;
    


由于涉及到多线程,如果我们暂时不创建多线程的话,那么是演示不出来效果的,而且即使创建出来多线程,也不一定能演示出来效果,所以,我们只好做如下分析了。

假设线程1现在拿到了cpu的执行权,它在调用getInstance方法时,首先肯定是要做一个判断的,做完判断之后,它会进入到判断里面。此时,如果线程2拿到了cpu的执行权,那么线程1就会处于等待状态,这样,当线程2调用getInstance方法时,线程2现在还能进入到判断里面吗?很显然,进不来,因为这里面有一个同步锁,而线程1还在判断里面等待,它并没有释放这个同步锁,所以线程2是进不来的,它只能在外面等着。那么这样的话,线程2就只能等线程1执行完,而线程1执行完,就意味着instance变量已经被赋好值了,如此一来,就解决掉了线程不安全的问题了。

因此,以上两种实现方式要进行选择的话,我们肯定首选线程安全的这种实现方式。

实现方式三:双重检查锁

接下来,我们来看一下懒汉式的第三种实现方式,即双重检查锁方式。在讲解双重检查锁方式之前,我们先讨论一下上面懒汉式线程安全的那种实现方式所存在的问题,也即在getInstance方法上加了锁之后所导致的性能问题。

先看下面这句话:

对于getInstance方法来说,绝大部分的操作都是读操作,而读操作本身就是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,故我们需要调整加锁的时机。

看完上面这句话之后,有些同学可能不甚理解什么是读操作。没关系,我为大家解释一下,大家不妨看一下以上懒汉式的第二种实现方式所对应的代码,可以看到,第一次调用getInstance方法的时候,会先判断instance是否为null,此时肯定是为null的,所以就会创建一个Singleton类的对象,并将其赋给instance,这是一个赋值的操作,我们又可以称为写操作。

赋完值之后,接着就要把instance的值进行一个返回了,即将当前类的对象返回。后续每一次调用getInstance方法时,由于if判断语句中的条件不再成立(这是因为instance已经被赋予值了),所以都会直接将instance的值进行返回。于是,后续每一次调用getInstance方法,将instance直接返回的这个操作,我们就可以把它称为读操作了。

明确了读操作之后,我们继续看以上那句话的后半段描述,说我们没必要让每个线程必须持有锁才能调用getInstance方法,为什么呢?因为这会导致程序运行的性能更加低下。所以,我们才要调整加锁的时机,由此也产生了一种新的实现方式——双重检查锁方式。

既然是双重检查,那么肯定是两次判断了。接下来,我们就来看一下具体的代码实现。

首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo4,这表明编写的懒汉式的第三种实现方式的代码我们都会放在该包下。

然后,在com.meimeixia.pattern.singleton.demo4包下创建一个单例类,该类我们同样命名为Singleton。

同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。

  1. 私有构造方法
  2. 在成员位置声明一个该类的成员变量,不过不要给其赋值
  3. 对外提供一个公共的访问方式,在该方法里面记得要做两次判断

经过以上三个步骤,相信大家能写出来下面这样的Singleton类。

package com.meimeixia.pattern.singleton.demo4;

/**
 * 双重检查锁方式
 * @author liayun
 * @create 2021-05-29 18:48
 */
public class Singleton 
    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量
    private static Singleton instance;

    // 对外提供公共的访问方式
    public static Singleton getInstance() 
        // 第一次判断,如果instance的值不为null,那么就不需要抢占锁了,直接返回对象即可
        if (instance == null) 
            synchronized (Singleton.class) 
                // 第二次判断
                if (instance == null) 
                    instance = new Singleton();
                
            
        
        return instance;
    

可以看到,对于getInstance方法来说,我们是做了两次判断的。

  • 第一次判断:判断instance是否等于null,若是,则需抢占锁;若不是,则直接返回instance即可,由于并没有去持有锁,所以效率就可以得到提升了
  • 第二次判断:如果第一次判断时,instance等于null,那么进来判断里面之后我们得先持有一把锁,所以在这儿咱们就编写了一个同步代码块,而且锁对象就是当前类的字节码对象,这样,我们就可以在同步代码块里面做第二次判断了。所做判断很简单,无非就是判断instance是否为null,若是,则需创建一个本类对象并将其赋值给instance;若不是,则直接返回instance即可

双重检查锁方式的实现代码我们写完之后,不妨编写一个测试类来测试一下它有没有实现单例。

package com.meimeixia.pattern.singleton.demo4;

/**
 * @author liayun
 * @create 2021-05-29 19:01
 */
public class Client 
    public static void main(String[] args) 
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);
    

运行以上测试类之后,如下图所示,你会发现打印结果是true,这说明双重检查锁这种方式确实是实现了单例。

以上双重检查锁方式的实现代码虽然写完了,但你不觉得还存在什么问题吗?双重检查锁方式是一种非常好的单例实现方式,解决了单例、性能、线程安全问题,上面的双重检查锁方式看上去完美无缺,其实它是存在问题的,即在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

由于以上双重检查锁方式所存在的问题是在多线程的情况下有可能出现,有可能不出现,所以在这儿我就不为大家进行演示了。对了,还有引起该问题的原因中涉及到了一个指令重排序操作,在本课程里面我也不会做详细讲解,大家有兴趣的话不妨去看一下Java并发编程。

既然问题出现了,那如何进行解决呢?要解决双重检查锁方式所带来的空指针异常问题,只需要使用volatile关键字即可,因为该关键字的作用就是保证可见性和有序性,在此处体现的就是有序性。

那么问题又来了,volatile关键字我们应该放在哪呢?记住,我们应该放在instance变量上面,用它来修饰instance变量,这样,就可以保证指令是有序的了。

package com.meimeixia.pattern.singleton.demo4;

/**
 * 双重检查锁方式
 * @author liayun
 * @create 2021-05-29 18:48
 */
public class Singleton 
    // 私有构造方法
    private Singleton() 

    // 声明Singleton类型的变量
    private static volatile Singleton instance;

    // 对外提供公共的访问方式
    public static Singleton getInstance() 
        // 第一次判断,如果instance的值不为null,那么就不需要抢占锁了,直接返回对象即可
        if (instance == null) 
            synchronized (Singleton.class) 
                // 第二次判断
                if (instance == null) 
                    instance = new Singleton();
                
            
        
        return instance;
    

以上就是双重检查锁方式的标准代码。现在再去运行Client测试类,打印的结果肯定是true,这是毋庸置疑的。

最后,我做一个总结,添加volatile关键字之后的双重检查锁方式是一种比较好的单例实现方式,能够保证在多线程的情况下线程安全,并且也不会有性能问题

实现方式四:静态内部类

上面我为大家讲解了双重检查锁这种单例实现方式,不过实现代码写起来还是稍微有点麻烦的,所以,在这一小节,我就来为大家介绍懒汉式的第四种实现方式,即静态内部类的方式。

静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性或者方法被调用时才会被加载,并初始化其静态属性。当然,这个静态属性所要被赋予的值其实就是外部类的对象。

而且,由于该静态属性被static修饰了,所以保证其只会被实例化一次,这样,就严格保证了实例化顺序,顺便就解决了上面所说的指令重排序的问题。

接下来,我们就来看一下具体的代码实现。

首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo5,这表明编写的懒汉式的第四种实现方式的代码我们都会放在该包下。

然后,在com.meimeixia.pattern.singleton.demo5包下创建一个单例类,该类我们同样命名为Singleton。

同理,只要大家跟着我的思路一步一步来,相信你一定能创建出该类。

  1. 私有构造方法
  2. 定义一个静态内部类,并在内部类中声明并初始化外部类的对象
  3. 在外部类里面,对外提供一个公共的访问方式,供外界去获取本类对象

经过上述四步,相信你很快就能创建出Singleton类了。

package com.meimeixia.pattern.singleton.demo5;

/**
 * 静态内部类方式
 * @author liayun
 * @create 2021-05-29 19:17
 */
public class Singleton 
    // 私有构造方法
    private Singleton() 

    // 定义一个静态内部类
    private static class SingletonHolder 
        // 在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton(); // 为了防止外界对该静态属性进行修改,故在其上加了一个final关键字,也即意味着它就是一个常量了
    

    // 提供公共的访问方式
    public static Singleton getInstance() 
        return SingletonHolder.INSTANCE;
    

接着,创建一个测试类,同理我们不妨命名为Client。

package com.meimeixia.pattern.singleton.demo5;

/**
 * @author liayun
 * @create 2021-05-29 19:21
 */
public class Client 
    public static void main(String[] args) 
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);
    

紧接着,我们运行一下以上测试类,若打印结果是true,则两次获取的是同一个对象;若打印结果是false,则两次获取的不是同一个对象。如下图所示,可以看到打印结果是true,这说明多次获取到的就是同一个对象。

至此,静态内部类的这种实现方式我就给大家讲解完了。

那么,我们是在何时何地创建外部类的对象的啊?是在内部类里面声明了一个静态的外部类类型的属性之后再对其进行的初始化。由于我们是在外部类里面定义了一个静态内部类,所以当我们在getInstance方法里面通过内部类类名调用其静态属性时,JVM才会去加载该内部类,并且初始化其静态属性。这样,该静态属性就只会被初始化一次了,即使是在多线程情况下调用getInstance方法,最终获取到的都是同一个对象。

最后,我们可以简单的做一个总结,静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费

枚举方式

枚举类实现单例模式是极力推荐的单例实现方式,因为枚举类型是线程安全的(也即它不会存在线程安全问题),并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式;其二,枚举的写法非常简单,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现方式。至于如何去破坏,我后面就会讲到,大家也别着急。

接下来,我们就来看一下具体的代码实现。

首先,在com.meimeixia.pattern.singleton包下再创建一个子包,即demo6,这表明编写的枚举实现方式的代码我们都会放在该包下。

然后,在com.meimeixia.pattern.singleton.demo6包下创建一个枚举类型的类,不妨我们就命名为Singleton。

package com.meimeixia.pattern.singleton.demo6;

/**
 * 枚举实现方式
 * @author liayun
 * @create 2021-05-29 19:32
 */
public enum Singleton 
    INSTANCE;

大家可能心里会犯嘀咕,真有这么简单吗?嘿嘿😘,枚举方式的实现还真这么简单。

接着,创建一个测试类,同理我们不妨命名为Client。

package com.meimeixia.pattern.singleton.demo6;

/**
 * @author liayun
 * @create 2021-05-29 20:01
 */
public class Client 

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


紧接着,我们运行一下以上测试类,若打印结果是true,则两次获取的是同一个对象;若打印结果是false,则两次获取的不是同一个对象。如下图所示,可以看到打印结果是true,这说明多次获取到的就是同一个对象。

最后,我得给大家说一个注意事项,就是枚举方式其实是属于饿汉式的,所以如果你不考虑浪费内存空间的话,那么就首选枚举这种实现方式吧!

存在的问题

接下来,我们来看一下单例模式所存在的问题,即破坏单例模式。

如何去理解破坏单例模式呢?就是使上面定义的单例类(即Singleton)可以创建多个对象,若能创建多个对象,则说明就已经破坏单例模式了,因为单例模式是只能让一个类创建一个对象。不过要注意,枚举方式除外,这是因为枚举方式是JVM底层的一个实现,它已经把所有的问题都解决掉了。

那么,如何来破坏单例模式呢?有如下两种方式:

  • 序列化
  • 反射

下面我就挨个来为大家讲解。

问题演示

序列化破坏单例模式

在本小节,我就以静态内部类这种实现方式来为大家进行演示了。当然了,你还可以使用其他的实现方式,不过要注意,枚举方式除外。

先给大家看一下Singleton类的代码,注意,由于现在咱们要进行序列化操作,所以定义的单例类(即Singleton)必须实现序列化的接口。

package com.meimeixia.pattern.singleton.demo7;

import java.io.Serializable;

/**
 * 静态内部类方式

以上是关于从零开始学习Java设计模式 | 创建型模式篇:单例设计模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 创建型模式篇:原型模式

从零开始学习Java设计模式 | 创建型模式篇:原型模式

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式

从零开始学习Java设计模式 | 创建型模式篇:工厂方法模式