• @设计模式-单例模式


    title: 设计模式-单例模式
    author: Xoni
    abbrlink: 9a9d1c95
    tags:

    • 设计模式
      categories:
    • 设计模式
      date: 2021-09-06

    前言:

    Spring 框架中都用到了哪些设计模式?

    Spring 框架中使用到了大量的设计模式,下面列举了比较有代表性的:

    • 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
    • 代理设计模式 : Spring AOP 功能时与使用JDK的动态代理
    • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
    • 模板方法模式 : 用来解决代码重复的问题,Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
    • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
    • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
    • 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
    • ……
    • 单例设计模式在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。使用单例模式的好处:

      • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
      • 由于 new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
    • 代理设计模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司
      模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

    • 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。

    • 观察者模式是一种对象行为型模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。Spring
      事件驱动模型就是观察者模式很经典的一个应用。Spring
      事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。

    • 适配器模式 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

    • 装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,就可以使用装饰者模式

    什么是单例模式

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象(自行实例化)。

    **关键代码:**构造函数是私有的。

    特点

    • 某个类只能有一个实例
    • 必须自行创建这个实例
    • 必须自行向整个系统提供这个实例

    作用

    1. 控制资源的使用,通过线程同步来控制资源的并发访问;
    2. 控制实例产生的数量,达到节约资源的目的。
    3. 作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。

    适用场景

    • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
    • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例
    • 在我看来,单例模式就类似于全局变量或全局函数的角色,可以使用它来代替全局变量

    优缺点

    优点
    • 提供了对唯一实例的受控访问。
    • 由于在系统内存中只存在一个对象,因此可以节约系统资源对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
    • 允许可变数目的实例。

    缺点
    • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
    • 单例类的职责过重,在一定程度上违背了“单一职责原则“。
    • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

    实现方式

    1. 懒汉式,线程不安全

    这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

    public class Singleton {  
        private static Singleton instance;  
        private Singleton (){}  
      
        public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。

    2. 懒汉式,线程安全

    这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

    优点:第一次调用才初始化,避免内存浪费。
    缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

    public class Singleton {  
        private static Singleton instance;  
        private Singleton (){}  
        public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3. 饿汉式

    这种方式比较常用,但容易产生垃圾对象。
    优点:没有加锁,执行效率会提高。
    缺点:类加载时就初始化,浪费内存。

    public class Singleton {  
        private static Singleton instance = new Singleton();  
        private Singleton (){}  
        public static Singleton getInstance() {  
        return instance;  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    4. 饿汉式(变种)

    这种也比较常用,static静态块只会的Singleton类创建的时候执行一次。

    public class Singleton {  
        private Singleton instance = null;  
    
        static {  
        instance = new Singleton();  
        }  
    
        private Singleton (){}  
    
        public static Singleton getInstance() {  
          return this.instance;  
        }  
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    表面上看起来差别挺大,其实和第三种方式差不多,都是在类初始化即实例化instance。

    5. 静态内部类

    这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    public class Singleton {  
        private static class SingletonHolder {  
    
        private static final Singleton INSTANCE = new Singleton();  
    
        }  
    
        private Singleton (){}  
    
        public static final Singleton getInstance() {  
          return SingletonHolder.INSTANCE;  
        }  
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
    通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)

    6. 枚举

    这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
    这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
    不能通过 reflection attack 来调用私有构造方法。

    public enum Singleton {  
        INSTANCE;  
        public void whateverMethod() {  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    一般情况下,不建议使用懒汉方式,建议使用种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。

    7. 双重校验锁

    这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

    public class Singleton {  
        private volatile static Singleton singleton;  
        private Singleton (){}  
        public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
            if (singleton == null) {  
                singleton = new Singleton();  
            }  
            }  
        }  
        return singleton;  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在JDK1.5之后,双重检查锁定才能够正常达到单例效果。

    总结

    第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时,可以试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。

    不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。懒汉,饿汉,双重校验锁,枚举和静态内部类。

    饿汉式和懒汉式
    • 饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例。从加载时间角度来讲,懒汉式单例加载时间也可能会比较长。

    • 懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是有多线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

  • 相关阅读:
    Android音视频开发系列:手撕FFmpeg音视频编解码
    关于Redis Lettuce连接池的问题
    Photoshop使用笔记总目录
    基于人脸识别的情绪社区(Python+Django+Mysql+Keras,tensorflow)
    Java版企业电子招标采购系统源码Spring Cloud + Spring Boot +二次开发+ MybatisPlus + Redis
    如何实现上拉加载,下拉刷新?
    【C++杂货铺】一文带你走进RBTree
    二叉排序树的插入(递归、非递归)和构造
    OpenLayers实战,OpenLayers调用手机陀螺仪方向实现指南针效果
    自动驾驶:控制算法概述
  • 原文地址:https://blog.csdn.net/weixin_45992021/article/details/126731632