详解Java设计模式之单例模式(Singleton Pattern)

x33g5p2x  于2021-12-30 转载在 Java  
字(4.1k)|赞(0)|评价(0)|浏览(213)

大家在使用Windows的时候不知道有没有注意过一个细节,在我们使用任务管理器的时候没有办法同时打开两个,也就是说,它在整个系统中只有唯一的一个实例。

对于系统中的某些类来说,只有一个实例很重要 !

例如:

  • 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
  • 一个班级只有一个班主任。
  • 在 Windows中就只能打开一个任务管理器(如图上图所示)。

如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源,如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此,有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

模式定义

单例模式是一种对象创建型模式。

  • 单例模式确保某一个类只有一个实例,而且自行实例化并向整个例,这个类称为单例类,它提供全局访问的方法。

单例模式的要点有三个:

  1. 有一个实例。
  2. 它必须自行创建这个实例。
  3. 它必须自行向整个系统提供这个实例。

模式结构

单例模式只包含一个 Singleton(单例角色)类,在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以使用它的唯一实例。

为了防止在外部对其实例化,将其构造函数设计为私有,在单例类内部定义了一个 Singleton类型的静态对象,作为外部共享的唯一实例。

一般情况下单例模式的代码实现如下 👇

package singleton;

/** * @author mengzhichao * @create 2021-11-28-15:47 */
public class Singleton {

    private static Singleton instance=null; //静态私有成员变量

    //私有构造函数
    private Singleton(){

    }

    //静态共有工厂方法,返回唯一实例
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

为了测试单例类所创建对象的唯一性,可以编写如下客户端测试代码

package singleton;

/** * @author mengzhichao * @create 2021-11-28-15:51 */
public class Client {
    public static void main(String[] args) {
        Singleton s1=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
        System.out.println(s1==s2);
    }
}

编译代码并运行,输出结果为:true

  • 说明两次调用getInstance()时所获取的对象是同一实例对象,且无法在外部不Singleton进行实例化,因而确保系统中只有唯一的一个Singleton对象。

在单例模式的实现过程中,需要注意以下几点:

  1. 单例类的构造函数为私有。
  2. 提供一个自身的静态私有成员变量。
  3. 提供一个公有的静态工厂方法。
  4. getInstance()方法中需要使用同步锁synchronized (Singleton.class)防止多线程同时进入造成 instance 被多次实例化。

应用场景

场景一

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
    场景二

  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

使用单例模式有一个必要条件: 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式
不适用场景

  • 不要使用单例模式存取全局变量,因为这违背了单例模式的用意,最好将全局变量放到对应类的静态成员中。
  • 不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。单例模式由于使用静态成员存储类的实例,所以可能会造成资源无法及时释放,带来一些问题。

模式优缺点

主要优点在于提供了对唯一实例的受控访问并可以节约系统资源。

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 避免对资源的多重占用(比如写文件操作)

其主要缺点在于因为缺少抽象层而难以扩展,且单例类职责过重。

  • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

模式案例

一.JDK中单例模式的应用实例

  • java. lang.Runtime类。

在每一个Java应用程序里面,都有唯一的一个Runtime对象,通过这个Runtime对象,应用程序可以与其运行环境发生相互作用。在JDK中,Runtime类的源代码片段如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    ...
}

二.Spring中的应用实例

当我们试图要从 Spring容器中获取某个类的实例时,默认情况下Spring会通过单例模式进行创建,也就是在Spring 的 bean 工厂中这个bean的实例只有一个,代码如下:

<bean id="date" class="java.util.Date" scope="singleton"/>

三.单例模式案例之身份证号码(练习)

  • 在现实生活中,居民身份证号码具有唯一性,同一个人不允许有多个身份证号码,第一次申请身份证时将给居民分配一个身份证号码,如果之后因为遗失等原因补办时,还是使用原来的身份证号码,不会产生新的号码。

现使用单例模式模拟该场景。

  1. 创建单例类 IdentityCardNo(身份证号码类)

在单例类IdentityCardNo中除了静态工厂方法外,还可以包含一些其他业务方法﹐如本例中的setIdentityCardNo()方法和 getIdentityCardNo()方法。在工厂方法 getInstance()中,先判断对象是否存在,如果不存在则实例化一个新的对象﹐然后返回;如果存在则直接返回已经存在的对象。

package singletontest;

/** * @author mengzhichao * @create 2021-11-28-16:18 */
public class IdentityCardNo {

    private static IdentityCardNo instance = null;

    private String no;

    private IdentityCardNo() {
    }

    public static IdentityCardNo getInstance(){
        if (instance==null){
            System.out.println("第一次办理身份证,分配新号码");
            instance=new IdentityCardNo();
            instance.setIdentityCardNo("No410111111122222222");
        }else {
            System.out.println("重复办理身份证,获取旧号码!");
        }
        return instance;
    }

    private void setIdentityCardNo(String no){
        this.no=no;
    }

    public String getIdentityCardNo(){
        return this.no;
    }

}
  1. 测试类 Client

在客户端测试代码中定义了两个IdentityCardNo类型的对象,通过调用两次静态工厂方法 getInstance()获取对象,然后判断它们是否相等﹔再通过业务方法 getIdentityCardNo()获取封装在对象中的属性号码no值,判断两次no值是否相同。

package singletontest;

/** * @author mengzhichao * @create 2021-11-28-16:26 */
public class Client {
    public static void main(String[] args) {
        IdentityCardNo no1,no2;
        no1=IdentityCardNo.getInstance();
        no2=IdentityCardNo.getInstance();
        System.out.println("身份证号码是否一致:" + (no1==no2));

        String str1,str2;
        str1=no1.getIdentityCardNo();
        str2=no2.getIdentityCardNo();
        System.out.println("第一次号码:"+str1);
        System.out.println("第二次号码:"+str2);

        System.out.println("内容是否相同:"+str1.equalsIgnoreCase(str2));
        System.out.println("对象是否相同:"+ (str1==str2));
    }
}
  1. 运行结果

从结果可以看出,两次创建的IdentityCardNo对象内存地址相同,是同一个对象,封装在其中的号码no属性不仅值相等,其内存地址也一致,是同一个成员属性。

学习更多设计模式还请访问https://blog.csdn.net/weixin_45692705?spm=1011.2124.3001.5343

相关文章