zoukankan      html  css  js  c++  java
  • 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效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊需求,可以考虑使用双检锁方式。




    只要你不停下来,慢一点也没关系。
  • 相关阅读:
    POJ 3126 Prime Path
    POJ 2429 GCD & LCM Inverse
    POJ 2395 Out of Hay
    【Codeforces 105D】 Bag of mice
    【POJ 3071】 Football
    【POJ 2096】 Collecting Bugs
    【CQOI 2009】 余数之和
    【Codeforces 258E】 Devu and Flowers
    【SDOI 2010】 古代猪文
    【BZOJ 2982】 combination
  • 原文地址:https://www.cnblogs.com/zyl-0110/p/14038315.html
Copyright © 2011-2022 走看看