• 每天学习一个设计模式(十二):创建型之单例模式


    一、基本概念

    单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

    二、通俗解释

    SINGLETON 单例模式:俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事) 单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的“单一实例”的需求时才可使用。

    皇帝的例子

    自从秦始皇确立了皇帝这个位置以后,同一时期基本上就只有一个人孤零零地坐在这个位置。这种情况下臣民们也好处理,大家叩拜、谈论的时候只要提及皇帝,每个人都知道指的是谁,而不用在皇帝前面加上特定的称呼,如张皇帝、李皇帝。这一个过程反应到设计领域就是,要求一个类只能生成一个对象(皇帝),所有对象对它的依赖都是相同的,因为只有一个对象,大家对它的脾气和习性都非常了解,建立健壮稳固的关系,我们把皇帝这种特殊职业通过程序来实现。皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象复制、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程类图如图所示。

    只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。

    皇帝类

    public class Emperor {
        /**
         * 初始化一个皇帝
         */
        private static final Emperor EMPEROR = new Emperor();
        private Emperor(){
            //世俗和道德约束你,目的就是不希望产生第二个皇帝
        }
        public static Emperor getInstance(){
            return EMPEROR;
        }
    
        /**
         * 皇帝发话了
         */
        public static void say(){
            System.out.println("我就是皇帝某某某...");
        }
    }

    通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。皇帝有了,臣子要出场。

    臣子类

    public class Minister {
        public static void main(String[] args) {
            for (int day = 0; day < 3; day++) {
                Emperor emperor = Emperor.getInstance();
                emperor.say();
            }
            //三天见的皇帝都是同一个人,荣幸吧!
        }
    }

    臣子参拜皇帝的运行结果如下所示。

    我就是皇帝某某某...

    我就是皇帝某某某...

    我就是皇帝某某某...

    臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。

    三、单例模式详解

    1.饿汉式单例模式

    饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。

    接下来看饿汉式单例的标准代码:

    /**
     * 优点:执行效率高,性能高,没有任何的锁
     * 缺点:某些情况下,可能会造成内存浪费
     */
    public class HungrySingleton {
    
        private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
    
        private HungrySingleton(){}
    
        public static HungrySingleton getInstance(){
            return HUNGRY_SINGLETON;
        }
    }

    还有另外一种写法,利用静态代码块的机制:

    public class HungryStaticSingleton {
        //先静态后动态
        //先上,后下
        //先属性后方法
        private static final HungryStaticSingleton HUNGRY_STATIC_SINGLETON;
    
        //装个B
        static {
            HUNGRY_STATIC_SINGLETON = new HungryStaticSingleton();
        }
    
        private HungryStaticSingleton(){}
    
        public static HungryStaticSingleton getInstance(){
            return HUNGRY_STATIC_SINGLETON;
        }
    }

    这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。这样一来,如果系统中有大批量的单例对象存在,那系统初始化时就会导致大量的内存浪费。也就是说,不管对象用与不用都占用着空间,浪费了内存,有可能“占着茅坑布拉斯”。那有没有更优的写法呢?下面继续分析。

    2.懒汉式单例模式

    为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现。

    /**
     * 优点:节省了内存,线程安全
     * 缺点:性能低
     */
    public class LazySimpleSingletion {
        private static LazySimpleSingletion instance = null;
        private LazySimpleSingletion(){}
    
        public static LazySimpleSingletion getInstance(){
            if(instance == null){
                instance = new LazySimpleSingletion();
            }
            return instance;
        }
    }

    但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我们来模拟下。

    线程类

    public class ExcutorThread implements Runnable {
        @Override
        public void run() {
            LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
            System.out.println(Thread.currentThread().getName() + ":" + singleton);
        }
    }

    测试类

    public class Client {
        public static void main(String[] args) {
            Thread t1 = new Thread(new ExcutorThread());
            Thread t2 = new Thread(new ExcutorThread());
            t1.start();
            t2.start();
            System.out.println("End");
        }
    }

    运行结果如下:

    End
    Thread-0:com.example.designpatterntest.factorymethod.singleton.LazySimpleSingleton@1122c7b
    Thread-1:com.example.designpatterntest.factorymethod.singleton.LazySimpleSingleton@1122c7b

    果然,上面的代码有一定的概率出现两种不同的结果(多试一试),这意味着上面的单例存在线程安全隐患。有人说可以加锁啊,但是用synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。接下来继续优化。

    3.双重检查锁单例模式

    /**
     * 优点:性能高了,线程安全了
     * 缺点:可读性难度加大,不够优雅
     */
    public class LazyDoubleCheckSingleton {
        private volatile static LazyDoubleCheckSingleton instance;
        private LazyDoubleCheckSingleton(){}
    
        public static LazyDoubleCheckSingleton getInstance(){
            //检查是否要阻塞
            if (instance == null) {
                synchronized (LazyDoubleCheckSingleton.class) {
                    //检查是否要重新创建实例
                    if (instance == null) {
                        instance = new LazyDoubleCheckSingleton();
                        //指令重排序的问题
                    }
                }
            }
            return instance;
        }
    }

    但是,只要用到了synchronized关键字总归要上锁,对程序性能还是存在一定影响。接下来继续优化。

    4.静态内部类单例模式

    /**
     * 优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费,不能被反射破坏
     * 缺点:不优雅
     */
    public class LazyStaticInnerClassSingleton {
    
        private LazyStaticInnerClassSingleton(){
            if(LazyHolder.INSTANCE != null){
                throw new RuntimeException("不允许非法访问");
            }
        }
    
        private static LazyStaticInnerClassSingleton getInstance(){
            return LazyHolder.INSTANCE;
        }
    
        private static class LazyHolder{
            private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
        }
    }

    这种方式兼顾了饿汉式单例模式的内存浪费问题和上锁带来的性能问题。内部类一定是要在方法调用之前初始化,巧妙的避免了线程安全问题。但是,“人无完人,类无完类”,这种方式虽然可以避免反射破坏,却无法避免序列化破坏。一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的对象为单例,就违背了单例模式的初衷,相当于破坏了单例。其实很简单,java已经为我们考虑到了这个问题,只需要在单例类中增加readResolve()方法即可,这里不做解释。

    但是通过JDK源码可以看出,虽然readResolve()方法返回实例解决了单例模式被破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,接下来继续优化。

    5.注册式单例模式

    注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一标识获取实例。注册式单例模式又分为两种:枚举式和容器式。

    枚举式单例模式

    直接上代码:

    public enum EnumSingleton {
        //
        INSTANCE;
    
        private Object data;
    
        public Object getData() {
            return data;
        }
        
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    

    看下测试代码:

    public class EnumSingletonTest {
        public static void main(String[] args) {
            try {
                EnumSingleton instance1 = null;
                EnumSingleton instance2 = EnumSingleton.getInstance();
                instance2.setData(new Object());
    
                FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(instance2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("EnumSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                instance1 = (EnumSingleton) ois.readObject();
                ois.close();
    
                System.out.println(instance1.getData());
                System.out.println(instance2.getData());
                System.out.println(instance1.getData() == instance2.getData());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    运行结果如下:

    java.lang.Object@19d3b25
    java.lang.Object@19d3b25
    true

    完美的解决了反序列化问题。而且你也可以试试,枚举式单例类也不能用反射来创建的。原因很简单,可以看java.lang.Enum的源码,你会发现它只有一个protected类型的构造方法。枚举式单例模式也是《Effective Java》中推荐的一种单例模式实现写法,但是我们业务开发中见的少,这种模式虽然写法优雅,但是也不是完美的。因为它在类加载时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。绕了一大圈,你会发现又回到了原点,是不是很神奇,这说明了一个道理”万变不离其宗“。

    容器式单例模式

    直接上代码:

    public class ContainerSingleton {
    
        private ContainerSingleton(){}
    
        private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    
        public static Object getInstance(String className){
            Object instance = null;
            if(!ioc.containsKey(className)){
                try {
                    instance = Class.forName(className).newInstance();
                    ioc.put(className, instance);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return instance;
            }else{
                return ioc.get(className);
            }
        }
    }

    该模式很明显是用了反射,适用于需要大量创建单例对象的场景,便于管理。但它又是非线程安全的。你又会发现兜兜转转又回到了懒汉式的问题所在的地方。

    总结  看似简单的单例模式其实也有很多门道,所以小伙伴们面试中会经常遇到,不管是哪样单例模式,都是为了保证内存里只有一个实例,减少内存的开销,避免对资源的多重占用。业务中具体用哪样单例模式,仁者见仁。


    参考秦小波的《设计模式之禅(第2版)》

  • 相关阅读:
    python_函数
    初始python第三天(三)
    python入门练习题2
    python开发进阶之路(一)
    python入门练习题1
    初识Python第三天(二)
    初识Python第三天(一)
    初识Python第二天(4)
    初识python第二天(3)
    c windows控制台输出颜色文字
  • 原文地址:https://www.cnblogs.com/aohongzhu/p/15174398.html
Copyright © 2020-2023  润新知