前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >程序员羽化之路--我眼中的单例模式并不完美(文末送书)

程序员羽化之路--我眼中的单例模式并不完美(文末送书)

作者头像
架构师修行之路
发布2020-07-07 12:20:04
3750
发布2020-07-07 12:20:04
举报
文章被收录于专栏:架构师

菜菜哥,帮忙解决一个问题,我写的一个程序领导说有线程安全问题,改不好不允许上线

什么样的程序呢?

代码语言:javascript
复制
/// <summary>
    /// 全局唯一的配置信息
    /// </summary>
   public class Config
    {
        private static Config _config = null;
        public static Config GetConfig()
        {
            if (_config == null)
            {
                _config = new Config();
            }
            return _config;
        }
    }

我的程序中有一个全局唯一的配置类,用来存储配置信息

看样子你是需要一个单例类吧,但是你这个代码对于实现单例模式有很大问题啊

01

PART

单例模式

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

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

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

02

PART

单例实现

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

1. private的构造函数,主要是为了避免外部通过New创建实例

2. 单例是否支持延迟加载,以及性能是否高效

3. 多线程环境下,对象创建是否安全

4. 全局只有一个访问入口

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

饿汉式

代码语言:javascript
复制
public class Config
    {
        private Config() { }
        private static Config _config = new Config ();
        public static Config GetConfig()
        {

            return _config;
        }
    }

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

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

懒汉式

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

代码语言:javascript
复制
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;
        }

双重加锁机制

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

代码语言:javascript
复制
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的内部类,静态初始化特性等都可以实现线程安全的单例模式。

单例模式居然还有这么多事,那以后我要深入研究一下了

单例模式虽然是一种比较常用的模式,但是从很多使用维度来说,有很多问题

03

PART

单例模式缺陷

1. 面向对象设计讲究封装,继承,多态,以及抽象。单例模式对于其中的继承,多态支持得不好,抽象讲究的是面向接口编程,单例模式并没有接口概念。拿以上配置文件的单例为例,假设现在的配置信息是以本地文件的方式进行加载,如果后期要加入从数据库加载配置信息这个需求,单例模式必须修改现有代码,这在一定程度上就违反了设计规则。所以单例在一定程度上丢失了应对未来需求的扩张性。

2. 单例模式在职责上有时候会过重,即要负责初始化的过程,又要负责初始化的内容,甚至在某些情况下还要负责其他程序,这在一定程度上违反了“单一职责”原则。

3. 由于单例模式对外之后一个入口点,并没有显示的利用构造函数传参的方式进行初始化,内部使用了哪些类型并不能很快识别出来,开发人员很难识别出类的依赖关系

4. 单例模式并不适合那些表面是单例,但是未来还有可能扩展的场景。举个栗子:线程池在很多程序中都被设计成单例模式,很多开发人员认为程序中只存在一个线程池,但是在个别需求下,同一个程序需要多个线程池的场景是存在的。

04

PART

写在最后

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 架构师修行之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档