美文网首页
我眼中的单例模式并不完美

我眼中的单例模式并不完美

作者: 架构师修行之路 | 来源:发表于2020-11-10 21:56 被阅读0次
    image
    /// <summary>
        /// 全局唯一的配置信息
        /// </summary>
       public class Config
        {
            private static Config _config = null;
            public static Config GetConfig()
            {
                if (_config == null)
                {
                    _config = new Config();
                }
                return _config;
            }
        }
    
    image

    单例模式

    所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。

    几乎大部分程序员面试的时候,面试官让你说出三种常用的设计模式,单例必是其中之一。平时所说的单例模式是指一个进程内只存在某个类型的一个实例,其实扩展到集群这个概念,位于不同物理环境的多个进程之间也可以有单例这种概念,像平时吹水用的分布式锁其实可以看做是多进程之间的单例模式。

    透过单例的现象可以看到单例模式本质上也是解决资源竞争的问题,它让多个线程甚至多个进程共享同一个资源,以达到资源共享的目的。为什么要实现资源共享呢?因为这个资源在业务场景下只能存在一个实例,例如以上的全局配置信息,如果程序内部有多个配置信息实例,不仅浪费了服务器的内存资源,在配置信息发生修改的时候,多个实例随之同步更新又是一个很大的问题。

    单例实现

    单例模式的概念很简单,实现的方式也有很多,但是关注点却无外乎以下几个:

    1. private的构造函数,主要是为了避免外部通过New创建实例
    2. 单例是否支持延迟加载,以及性能是否高效
    3. 多线程环境下,对象创建是否安全
    4. 全局只有一个访问入口

    为了达到以上几个要求,我们可以有很多的实现方法

    饿汉式
    public class Config
        {
            private Config() { }
            private static Config _config = new Config ();
            public static Config GetConfig()
            {
               
                return _config;
            }
        }
    

    这种方式主要是利用了语言特性,一个类型的静态属性是属于类型所有,在类的生命周期内只会加载一次,所以以上代码实现单例模式并没有问题,而且超级简单。

    很多人说这种方式不妥,在类型的加载时候就完成实例创建,没有达到惰性加载,会造成内存的浪费。至于这个问题我并不表示完全赞同。如果一个单例的初始化耗时比较长,最好不要等到真正用它的时候才去执行初始化,这样会影响系统的性能。饿汉式可以实现在程序启动的时候就进行初始化操作,这样就能避免初始化时间过长导致的性能问题,而且还有一个比较重要的好处,如果初始化程序有错误,我们可以在程序启动的时候就发现,而不用等到程序上线运行时才暴露出来。这就好比编译期错误永远比运行时错误好排查的道理类似。

    懒汉式

    程序员妹子贡献的代码其实就属于懒汉式,表面上看可以实现惰性加载,但是在多线程的环境下,会产生多个实例,问题就在于 if (_config == null) 这个语句并非是线程安全的。如果非要改造的话,可以加上全局的锁机制,有一个注意点,这里锁的对象一定要是一个static全局的对象

     private static object objLock = new object();
            private static Config _config = null;
            public static Config GetConfig()
            {
                lock (objLock)
                {
                    if (_config == null)
                    {
                        _config = new Config();
                    }
                }           
                return _config;
            }
    
    双重加锁机制

    虽然懒汉式方式能保证线程安全,但是锁的机制缺大大降低了系统性能,原因是锁机制把所有请求顺序化了,为了改善懒汉式的性能,所以双重加锁机制出现了,在保证了线程安全的情况下,大大提高了程序性能

    private static object objLock = new object();
            private static Config _config = null;
            public static Config GetConfig()
            {
                if (_config == null)
                {
                    lock (objLock)
                    {
                        if (_config == null)
                        {
                            _config = new Config();
                        }
                    }
                }
                    
                return _config;
            }
    

    以上只是实现单例的几种常用方式,根据每个语言的特性还有很多可以实现单例的方式,比如:利用c#或者java的内部类,静态初始化特性等都可以实现线程安全的单例模式。

    image

    单例模式缺陷

    1. 面向对象设计讲究封装,继承,多态,以及抽象。单例模式对于其中的继承,多态支持得不好,抽象讲究的是面向接口编程,单例模式并没有接口概念。拿以上配置文件的单例为例,假设现在的配置信息是以本地文件的方式进行加载,如果后期要加入从数据库加载配置信息这个需求,单例模式必须修改现有代码,这在一定程度上就违反了设计规则。所以单例在一定程度上丢失了应对未来需求的扩张性。
    2. 单例模式在职责上有时候会过重,即要负责初始化的过程,又要负责初始化的内容,甚至在某些情况下还要负责其他程序,这在一定程度上违反了“单一职责”原则。
    3. 由于单例模式对外之后一个入口点,并没有显示的利用构造函数传参的方式进行初始化,内部使用了哪些类型并不能很快识别出来,开发人员很难识别出类的依赖关系
    4. 单例模式并不适合那些表面是单例,但是未来还有可能扩展的场景。举个栗子:线程池在很多程序中都被设计成单例模式,很多开发人员认为程序中只存在一个线程池,但是在个别需求下,同一个程序需要多个线程池的场景是存在的。

    写在最后

    单例模式最为常用的一种模式,有其自己的优势和适用场景。如果一个类型在程序中要求实例化的数量有要求的,该怎么办呢?比如,一个类型可以最多实例化10个,或者每个线程可以实例化一个,你可能需要研究一下threadLocal 或者hashmap等知识了。至于集群间的单例实现欢迎大家在留言区体现!!

    更多精彩文章

    相关文章

      网友评论

          本文标题:我眼中的单例模式并不完美

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