深入理解设计模式-创建型之单例模式

为什么要使用单例

1、表示全局唯一

如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。

  • 如:配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应该被映射为一个唯一的【配置实例】,此时就可以使用单例,当然也可以不用。
  • 全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功能。若计数器不唯一,很有可能产生统计无效,ID重复等。

2、处理资源访问冲突

如果让我们设计一个日志输出的功能:
多个logger实例,在多个线程中,同时操作同一个文件,就可能产生相互覆盖的问题。因为tomcat处理每一个请求都会使用一个新的线程(暂且不考虑多路复用)。此时日志文件就成了一个共享资源,但凡是多线程访问共享资源,我们都要考虑并发修改产生的问题。

源码应用

事实上,我们在JDK或者其他的通用框架中很少能看到标准的单例设计模式,这也就意味着他确实很经典,但严格的单例设计确实有它的问题和局限性,我们先看看在源码中的一些案例。

1、jdk的中的单例

jdk中有一个类的实现是一个标准单例模式->Runtime类,该类封装了运行时的环境。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。 一般不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过getRuntime 方法获取当前Runtime运行时对象的引用。

public class Runtime {
    // 典型的饿汉式

    private static final Runtime currentRuntime = new Runtime();
    private static Version version;
    public static Runtime getRuntime() {
        return currentRuntime;
   }
    /** Don't let anyone else instantiate this class */

    private Runtime() {}
    public void exit(int status) {
        @SuppressWarnings("removal")
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkExit(status);
       }
        Shutdown.exit(status);
   }
   
    public Process exec(String command) throws IOException {
        return exec(command, null, null);
   }
    public native long freeMemory();
    public native long maxMemory();
    public native void gc();
   
}

单例存在的问题

尽管单例是一个很经典的设计模式,但在实际的开发中,我们也很少按照严格的定义去使用它,以上的知识大多是为了理解和面试而使用和学习,有些人甚至认为单例是一种反模式(anti-pattern),压根就不推荐使用。大部分情况下,我们在项目中使用单例,**都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。**单例模式书写简洁、使用方便,在代码中,我们不需要创建对象。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题,所以我们一般会使用spring的单例容器作为替代方案。那单例究竟存在哪些
问题呢?

1、无法支持面向对象编程

我们知道,OOP 的三大特性是封装、继承、多态。单例将构造私有化,直接导致的结果就是,他无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,也就相当于损失了可以应对未来需求变化的扩展性,以后一旦有扩展需求,比如写一个
类似的具有绝大部分相同功能的单例,我们不得不新建一个十分【雷同】的单例。

2、极难的横向扩展

我们知道,单例类只能有一个对象实例。如果未来某一天,一个实例已经无法满足我们的需求,我们需要创建一个,或者更多个实例时,就必须对源代码进行修改,无法友好扩展。

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

不同作用范围的单例

首先,我们重新看一下单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?在标准的单例设计模式中,其单例是进程唯一的,也就意味着一个项目启动,在其整个运行环境中只能有一个实例。

事实上,在实际的工作当中,我们能够看到极多【只有一个实例的情况】,但是大多并不是标准的单例设计模式,如:

  • 1、使用ThreadLocal实现的线程级别的单一实例。

  • 2、使用spring实现的容器级别的单一是实例。

  • 3、使用分布式锁实现的集群状态的唯一实例。

以上的情况都不是标准的单例设计模式,但我们可以将其看做单例设计模式的扩展,我们以前两种情况为例进行介绍。

容器范围的单例

有的时候我们将单例的作用范围由进程切换到一个容器,可能会更加方便我们进行单例对象的管理。这也是spring作为java生态大哥大核心思想。spring通过提供一个单例容器,来确保一个实例在容器级别单例,并且可以在容器启动时完成初始化,他的优势如下:

1、所有的bean以单例形式存在于容器中,避免大量的对象被创建,造成jvm内存抖
动严重,频繁gc。

2、程序启动时,初始化单例bean,满足fast-fail,将所有构建过程的异常暴露在启
动时,而非运行时,更加安全。

3、缓存了所有单例bean,启动的过程相当于预热的过程,运行时不必进行对象创
建,效率更高。

4、容器管理bean的生命周期,结合依赖注入使得解耦更加彻底、扩展性无敌。

详解Java实现单例模式(面试题)懒汉式饿汉式

详解Java实现单例模式(面试题)懒汉式饿汉式

你可能感兴趣的:(设计模式,设计模式,单例模式)