美文网首页
设计模式之单例模式

设计模式之单例模式

作者: 戴先森Davi | 来源:发表于2020-01-04 19:57 被阅读0次

    单例模式

    单例模式定义

    • 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

    单例模式是“创建型”设计模式之一。

    单例模式的使用场景

    • 确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对
      象只应该有且只有一个。例如,创建一个对象需要消耗的资源过多,如要访问I0和数据库等资源,这时就要考虑使用单例模式。

    单例模式UML类图

    单例模式UML.png
    角色介绍:
    • Client高层客户端;
    • Singleton单例类。

    实现单例模式主要有如下几个关键点:

    1. 构造函数不对外开放,一般为Private;
    2. 通过一个静态方法或者枚举返回单例类对象;
    3. 确保单例类的对象有且只有一个,尤其是在多线程环境下;
    4. 确保单例类对象在反序列化时不会重新构建对象。

    通过将单例类的构造函数私有化,使得客户端代码不能通过new的形式手动构造单例类的对象。单例类会暴露一个公有静态方法,客户端需要调用这个静态方法获取到单例类的唯一对象,在获取这个单例对象的过程中需要确保线程安全,即在多线程环境下构造单例类的对象也是有且只有一个,这也是单例模式实现中比较困难的地方。

    单例类的实现

    1. 饿汉模式

    饿汉模式 - java版本

    public class SimpleSingleton {
        private static SimpleSingleton mInstance = new SimpleSingleton();
    
        private SimpleSingleton() {
        }
    
        public static SimpleSingleton getInstance() {
            return mInstance;
        }
    }
    

    饿汉模式 - kotlin版本

    object SimpleSingleton {
        fun dosomething() {
            println("dosomething")
        }
    }
    

    饿汉式优缺点

    优点:

    • 在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题

    缺点:

    • 如果构造方法中存在过多的处理,会导致加载这个类时比较慢,可能引起性能问题。
    • 如果使用饿汉式的话,只进行了类的装载,并没有实质的调用,会造成资源的浪费。

    适用场景:

    • 这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

    2. 懒汉模式

    懒汉模式 - java版本

    public class LazySingleton {
        private static LazySingleton mInstance = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (null == mInstance) {
                mInstance = new LazySingleton();
            }
            return mInstance;
        }
    }
    

    懒汉模式 - kotlin版本

    class LazySingleton {
        companion object {
            val mInstance: LazySingleton by lazy { LazySingleton() }
        }
    }
    

    说明:

    • 显式声明构造方法为private
    • companion object用来在class内部声明一个对象
    • LazySingleton的实例instance 通过lazy来实现懒汉式加载
    • lazy默认情况下是线程安全的,这就可以避免多个线程同时访问生成多个实例的问题

    懒汉式优缺点

    优点:

    • 懒汉模式中单例是在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象。

    缺点:

    • java版本的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题(getInstance()方法上加上synchronized字段)。

    适用场景:

    • 如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候使用懒汉模式就是一个不错的选择。

    3. 双重校验锁

    双重校验锁版本

    public class DoubleCheckSingleton {
        private static volatile DoubleCheckSingleton mInstance = null;
    
        private DoubleCheckSingleton() {
        }
    
        public static DoubleCheckSingleton getInstance() {
            if (mInstance == null) {
                synchronized (DoubleCheckSingleton.class) {
                    if (mInstance == null) {
                        // Double checked
                        mInstance = new DoubleCheckSingleton();
                    }
                }
            }
            return mInstance;
        }
    }
    
    • 双重校验锁解决了“懒汉式”单例模式的线程安全问题。
    • 增加了 volatile 关键字,禁止指令重排序优化。

    4. 静态内部类

    静态内部类版本

    public class StaticInnerSingleton {
        private static class SingletonHolder {
            static StaticInnerSingleton mInstance = new StaticInnerSingleton();
        }
    
        private StaticInnerSingleton() {
        }
    
        public static StaticInnerSingleton getInstance() {
            return SingletonHolder.mInstance;
        }
    }
    
    • 这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。
    • 不一样的是,它是在内部类里面去创建对象实例。
    • 这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

    5. 枚举单例

    public class EnumSingleton {
        private EnumSingleton() {
        }
    
        public static EnumSingleton getInstance() {
            return Singleton.INSTANCE.getInstance();
        }
    
        private enum Singleton {
            INSTANCE;
            private EnumSingleton singleton;
    
            //JVM会保证此方法绝对只调用一次
            private Singleton() {
                singleton = new EnumSingleton();
            }
    
            public EnumSingleton getInstance() {
                return singleton;
            }
        }
    }
    
    • 在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。
    • 同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时(Singleton.INSTANCE),我们的单例被实例化。
    • 也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。

    总结:

    上面提到的五种种实现单例的方式都有共同的缺点:

    • 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
    • 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

    而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

    单例模式的线程安全性:

    • 饿汉式:线程安全
    • 懒汉式:非线程安全
    • 双检锁:线程安全
    • 静态内部类:线程安全
    • 枚举:线程安全

    参考链接:
    https://blog.csdn.net/fly910905/article/details/79286680
    https://droidyue.com/blog/2017/07/17/singleton-in-kotlin/

    相关文章

      网友评论

          本文标题:设计模式之单例模式

          本文链接:https://www.haomeiwen.com/subject/zifroctx.html