写点什么

设计模式中的单例模式并不完美

发布于: 2020 年 08 月 06 日
设计模式中的单例模式并不完美





/// <summary>
/// 全局唯一的配置信息
/// </summary>
public class Config
{
private static Config _config = null;
public static Config GetConfig()
{
if (_config == null)
{
_config = new Config();
}
return _config;
}
}






单例模式



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



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



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

单例实现



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



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





单例模式缺陷



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

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

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

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

写在最后



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



点击这里查看更多精彩内容



发布于: 2020 年 08 月 06 日阅读数: 2107
用户头像

一个坚持把架构做到极致的技术人 2014.10.19 加入

还未添加个人简介

评论

发布
暂无评论
设计模式中的单例模式并不完美