zoukankan      html  css  js  c++  java
  • 单例模式

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

    单例模式

    一、特点

    1.1 属性

    意图保证一个类仅有一个实例,并提供一个访问它的全局访问点

    主要解决:一个全局使用的类频繁地创建与销毁

    何时使用:当您想控制实例数目,节省系统资源的时候。

    如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建

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

    应用实例:一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

    • 优点:

      1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
      2. 避免对资源的多重占用(比如写文件操作)。
    • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    • 使用场景:

      1. 要求生产唯一序列号
      2. WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
      3. 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
    • 注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。



    二、单例模式的实现

    三个步骤:

    1. 构造函数私有化
    2. 自行对外提供实例
    3. 提供外界可以获得该实例的方法

    2.1 传统的创建类的方式

    public class s1 {
        public static void main(String[] args) {
    
            Singleton singleton1 = new Singleton();
            Singleton singleton2 = new Singleton();
        }
    }
    
    class Singleton {
    
    }
    

    上述代码中,每次new Singleton()都会创建一个Signleton实例。

    2.2 恶汉模式

    是否 Lazy 初始化:否
    是否多线程安全:是
    实现难度:易

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

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

    lazy loading(延迟加载):例如创建某一对象时需要花费很大的开销,而这一对象在系统的运行过程中不一定会用到,这时就可以使用延迟加载,在第一次使用该对象时再对其进行初始化,如果没有用到则不需要进行初始化,这样的话,使用延迟初始化就提高程序的效率,从而使程序占用更少的内存。

    public class Singleton {
        private static Singleton instance = new Singleton();
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    2.3 懒汉模式

    是否Lazy初始化:是
    是否多线程安全:是
    实现难度:易

    • 描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是效率很低,99% 情况下不需要同步。
    • 优点:第一次调用才初始化,避免内存浪费
    • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率

    getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

    public class Singleton {
        private static Singleton instance;
    
        private Singleton() {
        }
    
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    2.4 双检锁/双重校验锁(DCL,即 double-checked locking)

    添加synchronized锁虽然可以保证线程安全,但是每次访问getInstance()方法的时候,都会有加锁和解锁操作,同时synchronized锁加在方法上面,锁的范围过大,会成为系统的瓶颈

    是否 Lazy 初始化:是
    是否多线程安全:是
    实现难度:较复杂

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

    getInstance() 的性能对应用程序很关键

    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;
        }
    }
    

    2.5 双检锁/双重校验锁(增加volatile)

    双重校验锁会出现指令重排的问题,singleton = new Singleton();并非一个原子操作,实际上,它可以抽象为下面几个JVM指令:

    //1. 分配对象内存空间
    memory = allocate();
    //2. 初始化对象
    ctorInstance(memory);
    //3. 设置instance指向刚分配的内存地址
    singleton = memory;
    

    操作2依赖于操作1,但操作3并不依赖于操作1,所以JVM是可以针对它们进行指令优化,优化后如下:

    //1. 分配对象内存空间
    memory = allocate();
    //3. 设置instance指向刚分配的内存地址
    singleton = memory;
    //2. 初始化对象
    ctorInstance(memory);
    

    可以看到,指令重排之后,singleton指向分配好的内存放在前面,而这段内存的初始化被排在了后面。
    线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值singleton引用,恰好线程B进入方法判断singleton的引用不为空,然后就将其返回使用,导致程序出错。
    为了解决指令重排问题,可以使用volatile关键字修饰singleton字段,禁止指令的重排序优化

    class Singleton {
        private static volatile Singleton singleton = null;
    
        private Singleton();
    
        public static Singleton getInstance() {
            if (singleton = null) {
                synchronized (Singleton.class) {
                    if (singleton = null) {
                        singleton = new Singleton();
                    }
                }
            }
            return signleton;
        }
    }
    
    

    2.6 静态内部类

    是否 Lazy 初始化:是
    是否多线程安全:是
    实现难度:一般

    描述:当第一次访问类中的静态字段时,会触发类加载,并且保证同一个类只加载一次。静态内部类也是如此,类加载过程有类加载器负责加锁。这种写法相对于双重检验锁的写法,更加简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    静态域:如果将域定义为static,每个类中只有一个这样的域,这个类的所有对象将共享这个域,这个域称为静态域。这个域属于类,而不属于任何独立的对象。

    class Singleton {
        //私有的静态内部类,类加载器负责加锁
        private static class SingletonHolder {
            private static Singleton singleton = new Singleton();
        }
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return Singleton.singleton;
        }
    }
    
    

    2.6 枚举

    是否 Lazy 初始化:否
    是否多线程安全:是
    实现难度:易

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

    public enum Singleton {
        INSTANCE;
    
        public void whateverMethod() {
        }
    }
    








    推荐阅读

    参考

    [1]黄文毅,Spring MVC + MyBatis快速开发与项目实战.北京:清华出版社,2019.
    [2]https://www.runoob.com/design-pattern/singleton-pattern.html
    [3]https://blog.csdn.net/u013894997/article/details/81111236
  • 相关阅读:
    HBase 列族数量为什么越少越好
    Hbase 认识及其作用
    Hbase 源码讲解
    Hbase 目录树
    rabbitmq 连接过程详解
    rabbit 兔子和兔子窝
    rabbit 函数参数详解
    rabbitmq 用户和授权
    火狐浏览器安装有道插件
    rabbitmq vhost
  • 原文地址:https://www.cnblogs.com/ccink/p/13367246.html
Copyright © 2011-2022 走看看