• 3单例模式


    单例模式

    单例模式(Singleleton Pattern) 是简单的一种设计模式。

    1单例模式的定义

    单例模式的英文原文是:
    Ensure a class has only one instance,and provide a global point of access to it.

    意思是:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

    单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接等需要单线程操作的场合,用于实现对系统资源的控制。

    由于Java的语言特点,使得在Java中实现单例模式通常有两种表现形式。
    • 饿汉式单例模式:类加载时,就进行对象实例化;
    • 懒汉式单例模式:第一次引用类时,才进行对象实例化。

    1.饿汉式单例类

    饿汉式单例模式-类图
    饿汉式源码
    package com.eric.创建型模式.单例模式.懒汉式;
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 饿汉式单例模式
     * @CreateTime 2020-11-25 15:42:47
     */
    public class Singleton {
        private static Singleton m_instance = new Singleton();
        //构造方法私有,保证外界无法直接实例化
        private Singleton(){}
        //通过该方法获取实例对象
        public static Singleton getInstance(){
            return m_instance;
        }
    }
    从上述代码中可以看到,在类被加载时,静态变量m_instance会被初始化,此时类的私有构造器会被调用,单例类的唯一实例就被创建出来了。单例类中最重要的特点是类的构造函数是私有的,从而避免外界利用构造函数直接创建出任意多的实例。另外需要注意的是,由于构造函数是私有的,因此该类不能被继承。

    2懒汉式单例类

    懒汉式单例类与饿汉式单例类相同的是,类的构造函数是私有的;不同的是,懒汉式单例类在加载不会将自己实例化,而是在第一次被调用时将自己实例化。(去掉 synchronized就是线程不安全的了)

    懒汉式单例
    package com.eric.创建型模式.单例模式.饿汉式;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 懒汉式
     * @CreateTime 2020-11-25 16:55:12
     */
    public class Singleton {
        private static Singleton _instance = null;
        //构造方法私有,保证外界无法直接实例化
        private Singleton(){}
        //方法同步
        synchronized public static Singleton getInstance(){
            if(_instance == null){
                _instance = new Singleton();
            }
            return _instance;
        }
    }
    上述代码中,懒汉式单例模式中对静态方法getInstance()进行同步,以确保多线程环境下只创建一个实例,例如,如果getInstance方法未被同步,并且线程A和线程B同时调用此方法,则执行if(_instance == null)语句都为真,那么线程A和线程B都会创建一个对象,在内存中就会出现两个对象,这样就违反了单例模式;但使用synchronized关键字同步后,则不会出现此种情况。

    饿汉式单例模式与懒汉式单例模式的区别。
    • 饿汉式单例模式在被加载时实例化,而懒汉式单例模式在第一次引用时被实例化。
    • 从资源利用效率上,饿汉式差一些;但从速度和执行时间来看,饿汉式要好一些。
    • 饿汉式单例模式可以在Java中实现,但不易在C++内实现。实际上饿汉式单例模式更符合Java语言本身的特点。

    2单例模式应用

    1.单例模式优点

    • 由于单例模式在内存中只有一个实例,减少了内存的开销,特别是一个对象需要频繁的创建、销毁,而且创建或销毁的性能有无法优化时,单例模式的优势就非常明显了。
    • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后永久驻留内存的方式来解决。
    • 单例模式可以避免多重占用,例如一个写文件动作,由于只有一个实例存在于内存中,避免了对同一个资源文件的同时操作。
    • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据库表的映射处理。    

    2.单例模式的缺点

    • 单例模式无法创建子类,扩展困难。若要扩展,除了修改代码基本没有第二种途径。
    • 单例模式对测试不利。在并行开发环境中,如果采用单例模式的类没有完成,是不能进行测试的;单例模式的类通常不会实现接口,这也妨碍了使用mock方式虚拟一个对象。
    • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不应该关心它是否是单例的,是不是要用单例模式取决于环境,单例模式把“要单例”和业务逻辑融合在一起。
    注意 单元测试时经常会采用stub和mock方式,这两种都可以对系统模块或单元进行隔离,通过创建虚拟的对象来模拟真实场景,一遍对测试对象进行测试工作。(stub和mock看其他资料)

    3.单例模式的使用场景

    在一个系统中,如果要求一个类有且仅有一个实例,当出现多个实例时就会造成不良反应,则此时可以采用单例模式。
    • 要求生成唯一序列号的环境。
    • 在整个项目中需要一个共享访问点或共享数据;例如Web页面上的计数器,可以不用吧每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保线程是安全的。
    • 创建一个对象需要消耗的资源过多时,如访问IO和数据库等资源。
    • 需要定义大量的静态常量和静态方法(如工具类的环境),可以采用单例模式(也乐意采用直接声明为static的方式)。
    单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring框架中,每个Bean默认就是单例的;Java基础类中的java.lang.Runtime类也采用了单例模式,其getRuntime()方法返回了唯一实例。

    4.使用单例模式的注意事项

    根据功能,单例类可以分为有状态单例模式和无状态模式。
    • 有状态单例类:一个有状态单例模式的对象一般是可变的,通常当做状态库使用。例如,给系统提供一个唯一的序列号。
    • 无状态单例类:无状态的单例模式的对象是不变的,通常用来提供工具性的功能方法。例如,IO或数据库库访问等。

    因为单例类具有状态,所以在使用时应注意以下两点
    • 单例类仅局限与一个JVM,因此当多个JVM的分布式时,这个单例类就会在多个JVM中被实例化,造成多个单例对象的出现。如果是无状态的单例类,则没有问题,因为这些单例对象是没有区别的。如果是有状态的但单例类,则会出现问题。如,给系统提供一个唯一序列号时,序列号不唯一,可能出现多次。因IC,在任何使用EJB、RMI和JINI技术的分布式系统中,应当避免使用有状态的单例类。
    • 同一个JVM中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例,此时也应尽量避免使用有状态的单例类。

    另外,使用单例模式时,需要注意序列化和克隆对实例唯一性的影响。如果一个单例的类实现了Serializable或Cloneable接口,则有可能被反序列化或克隆出一个新的实例来,从而破坏了“唯一实例”的要求,因此,通常单例类不需要实现Serializable或Cloneable接口。

    3单例模式实例

    例:使用单例模式记录访问次数
    GlobalNum.java
    package com.eric.创建型模式.单例模式.例1;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 全局计数器
     * @CreateTime 2020-11-25 18:51:54
     */
    public class GlobalNum {
        private static GlobalNum gn = new GlobalNum();
        private int num = 0;
        public static GlobalNum getInstance(){
            return gn;
        }
        public synchronized int getNum(){
            return ++num;
        }
    }
    上述代码中创建一个饿汉式单例类GlobalNum,其中getNum()方法用于返回访问次数,并且使用synchronized对该方法进行线程同步。

    NumThread.java
    package com.eric.创建型模式.单例模式.例1;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 线程类
     * @CreateTime 2020-11-25 18:56:57
     */
    public class NumThread extends Thread{
        private String threadName;
        public NumThread(String name){
            threadName = name;
        }
    //重写线程的run方法(线程任务)
        @Override
        public void run() {
            GlobalNum gnObj = GlobalNum.getInstance();
            //循环访问,输出访问次数
            for (int i = 0; i < 5; i++) {
                System.out.println(threadName+"第"+gnObj.getNum()+"次访问!");
                try{
                    this.sleep(1000);//线程休眠1s
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    SingleDemo.java
    package com.eric.创建型模式.单例模式.例1;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 测试单例模式
     * @CreateTime 2020-11-25 18:54:53
     */
    public class SingleDemo {
        //测试单例模式
        public static void main(String[] args) {
            //创建线程A
            NumThread a = new NumThread("线程A");
            //创建线程B
            NumThread b = new NumThread("线程B");
    
            //启动线程
            a.start();
            b.start();
        }
    }
    上述代码在主程序中创建两个子线程,通过这两个子线程演示对单例模式下唯一实例的访问。因为GlobalNum的对象是单例的,所以能够统一地对县城访问次数进行统计。
    由于上述代码是多线程的,运行结果每次都有可能出现不同,可能的运行结果。

    4其他几种单例模式的实现(重要!)

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

    JDK版本:JDK1.5起
    是否Lazy初始化:是
    是否线程安全:是
    实现难度:较复杂
    描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
    getInstance()的性能对应用程序很关键。
    package com.eric.创建型模式.单例模式.双重校验锁;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 双检锁/双重校验锁
     * @CreateTime 2020-11-25 19:19:58
     */
    public class Singleton {
        private volatile static Singleton singleton = null;
        private Singleton(){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    登记式/静态内部类

    是否Lazy初始化:是
    是否线程安全:是
    实现难度:一般
    描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
    这种方式同样利用了ClassLoader机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是:饿汉式只要Singleton类被装载了,那么instance就会被实例化(没有达到Lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。
    因为SingletonHolder类没有被主动使用,只有通过显式调用getInstance()方法时,才会显式装载SingletonHolder类,从而实例化instance。可以想象,如果实例化instance很消耗资源,所以想让他延迟加载,另一方面,又不希望在Singleton类加载时就实例化,因为不能确保Singleton类还可能在其他地方被主动使用从而被加载,那么这时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式就显得更合理。
    package com.eric.创建型模式.单例模式.登记式;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 登记式/静态内部类-----单例模式
     * @CreateTime 2020-11-25 19:51:40
     */
    public class Singleton {
        //静态内部类SingletonHolder
        private static class SingletonHolder{
            private static final Singleton INSTANCE = new Singleton();
        }
        //私有构造器
        private Singleton (){}
        
        public static final Singleton getInstance(){
            return SingletonHolder.INSTANCE;
        }
        
    }

    枚举

    JDK版本:JDK1.5起
    是否Lazy初始化:否
    是否线程安全:是
    实现难度:易
    描述:还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
    这种方式是Effective Java 作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止序列化重新创建新的对象,绝对防止多次实例化。不过由于JDK1.5之后才加入enum特性,用这种方式写,不免让人感到生疏,实际工作中,也很少用。
    不能通过reflection attack来调用私有构造方法。
    package com.eric.创建型模式.单例模式.枚举式;
    
    /**
     * @author Eric
     * @ProjectName my_design_23
     * @description 枚举方式的单例模式
     * @CreateTime 2020-11-25 20:02:05
     */
    public enum Singleton {
        INSTANCE;
        public void whateverMethod(){
            System.out.println("电脑开始做起了奇奇怪怪的事情...");
        }
    }
    [注]:
    一般情况下,不建议使用懒汉式建议使用饿汉式。只有在明确时限lazy loading效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊需求,可以考虑使用双检锁方式。




    只要你不停下来,慢一点也没关系。
  • 相关阅读:
    C#中的Singleton模式
    CodeLib
    Google Chats 居然和Gmail集成了...
    Windows中OSG环境搭建
    Socket中winsock.h和winsock2.h的不同
    高斯日记 蓝桥杯
    MATLAB矩阵处理
    马虎的算式 蓝桥杯
    MATLAB基础
    矩阵相乘的一维数组实现
  • 原文地址:https://www.cnblogs.com/zyl-0110/p/14038315.html
Copyright © 2020-2023  润新知