您的位置:首页 > 其它

【朝花夕拾】设计模式之单例模式

2019-04-05 12:05 501 查看
 

单例模式简介

单例模式是GOF 23个设计模式中最简单的模式了,它提供了一种创建唯一对象的最佳实现,注意此处的简单只是表述和意图很简单,但是实现起来,尤其是实现一个优美的单例模式却没有那么简单。

单例模式归根结底就是要确保一个类只有一个实例,并提供一个全局方式来访问该实例。具体而言,这种模式涉及到一个类,并由这个类创建自己的对象,同时确保只有单个对象被创建,并提供唯一一种方式来访问该对象的实例。

在现实生活中,单例的场景有很多,比如一夫一妻制(当然不道德的除外),比如一个部门只有一个领导等等。

单例模式UML类图

 如上图所示:

1、单例类只能有一个实例。

2、单例类必须自己创建自己的唯一实例。

3、单例类必须给所有其他对象提供这一实例。

4、构造函数是私有的。

范例

Double-Check

我们先看一个非常流行而又简单的实现

public sealed class Singleton
[code]{
private static Singleton instance = null;
private static readonly object padlock = new object();
 
private Singleton()
{
}
 
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
//Do a heavy task
}
}
}
return instance;
}
}
}
[/code]

上述解决方案上,使用到了Double-Check方式,Double-Check方式可以说是盛名已久了,线程A与线程B在Null Check时同时通过,但是在Lock时,只能进入一个线程,其他线程都要等着。

这种方式在Java中编写单例模式的时候是失效的,具体原因我没有去深究。这一块内存屏障技术(Memory Barrier),不过这段涉及到底层操作,一般很难有人会显式操作,而且这段的控制异常复杂。另外一点就是,如果单例过程中操作的是一个数组或者其他对象,那么在实例化后如果需要进行赋值等运算操作的,那么其他线程在进行Null Check的时候就不会再次进入,如果其他线程调用了这个单例对象的某个属性,这极有可能出现难以预测的bug

单例模式加载数据到内存,那么如果我们需要在使用的时候再去加载到内存,而不是一开始就加载到内存,这样可以节省内存空间。接下来我们看一下如何通过懒加载方式实现单例模式。

静态类

采用静态类实现单例模式,这并不是一种完全的懒加载,但依然是线程安全的
public sealed class Singleton
[code]{
private static readonly Singleton instance = new Singleton();
 
static Singleton()
{
 
}
 
private Singleton()
{
 
}
 
public static Singleton Instance
{
get
{
return instance;
}
}
}
[/code]

C#中的静态构造函数仅在创建类的实例或引用静态成员时执行,并且每个AppDomain只执行一次,因为每次都需要对新构造的类型执行这种检查,所以这种方式要比Double-Check方式更快。然而,也有一些问题:

  • 它不像其他实现那样懒惰。尤其是,如果您有实例以外的静态成员,那么对这些成员的第一个引用将涉及创建实例。这将在下一个实现中得到纠正。
  • 如果一个静态构造函数调用另一个静态构造函数,而另一个静态构造函数再次调用第一个静态构造函数,则会出现复杂情况。需要注意,静态构造函数在一个循环中相互引用的后果。
  • 只有当类型没有被[beforefieldinit]标记时,.NET才能保证类型初始值设定项的惰性。不幸的是,C编译器(至少在.NET 1.1运行时中提供)将没有静态构造函数的所有类型(即看起来像构造函数但被标记为静态的块)标记为beforefieldinit。需要注意beforefieldinit会影响性能,beforefieldinit的具体用法可以参见MSDN。

对于这个实现,许多人更喜欢拥有一个属性,以防将来需要进一步的操作,并且JIT内联可能使性能相同。另外有一种快捷方式就是,可以将实例设置为公共的静态只读变量,不设置为属性,这样代码的基本框架会显得非常小。(注意,如果需要惰性,静态构造函数本身仍然是必需的。)

内部类

采用内部类,这是一种完全的懒加载。

public sealed class Singleton
[code]{
private Singleton()
{
 
}
 
public static Singleton Instance { get { return Nested.instance; } }
 
private class Nested
{
static Nested()
{

}
 
internal static readonly Singleton instance = new Singleton();
}
}
[/code] .csharpcode,.csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff } .csharpcode pre { margin: 0em } .csharpcode .rem { color: #008000 } .csharpcode .kwrd { color: #0000ff } .csharpcode .str { color: #006080 } .csharpcode .op { color: #0000c0 } .csharpcode .preproc { color: #cc6633 } .csharpcode .asp { background-color: #ffff00 } .csharpcode .html { color: #800000 } .csharpcode .attr { color: #ff0000 } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em } .csharpcode .lnum { color: #606060 }

在这里,嵌套类的静态成员在第一次引用的时候会进行实例化操作,并且该引用只在实例中发生。这意味着实现是完全懒惰的,但具有前一个实现的所有性能优势。请注意,尽管嵌套类可以访问内部类的私有成员,但反过来却不是,因此需要在此处对实例进行内部访问。不过,这并不会引发任何问题,因为类本身是私有的。不过此处貌似显得有点复杂。

Lazy

那么有没有其他方式优雅而又安全的实现单例模式呢,答案是有的,那就是通过Lazy方式,Lazy方式可以拥有更高的性能,因为实例只有在使用的时候才会真正创建对象,这就在很大程度上减少了内存的占用,当然,比较如果是比较简单的单例创建,可以忽略这条不利影响。

Lazy自带Double-Check,是线程安全的,他就像一个盾牌,在创建过程中,不管是创建简单对象还是复杂对象,都不会允许其他线程使用尚未创建完成的对象,更多的Lazy使用,请参考MSDN。

public sealed class Singleton
[code]{
private Lazy<Singleton> lazy;
 
public Singleton Instance { get { return lazy.Value; } }
 
public string Name{get;set;}
 
public Singleton()
{
lazy = new Lazy<Singleton>(InitializeSingleton);
}
 
private Singleton InitializeSingleton()
{
Singleton singleton = new Singleton();
singleton.Name="Test";
return singleton;
}
}
[/code] .csharpcode,.csharpcode pre { font-size: small; color: black; font-family: consolas, "Courier New", courier, monospace; background-color: #ffffff } .csharpcode pre { margin: 0em } .csharpcode .rem { color: #008000 } .csharpcode .kwrd { color: #0000ff } .csharpcode .str { color: #006080 } .csharpcode .op { color: #0000c0 } .csharpcode .preproc { color: #cc6633 } .csharpcode .asp { background-color: #ffff00 } .csharpcode .html { color: #800000 } .csharpcode .attr { color: #ff0000 } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em } .csharpcode .lnum { color: #606060 }

单例模式优缺点

优点:

全局范围内只有一个实例,避免了内存消耗,以及实例频繁的创建和销毁

避免了对资源的多重占用,比如独占式场景中

 

缺点:

一旦对象指向的外部环境发生了变化,比如在网络调用、MQ等场景中一般可以可以采用单例,但是这里需要提醒的是,如果DNS发生异常,在异常期间将会出现极难修复的情况,除非手动重启并指向新的域服务器

这一点有点违反单一职责原则,通常情况下,一个类应该只关注自身逻辑而不是创建对象

没有接口,无法继承

本文参考了https://csharpindepth.com/articles/Singleton,该文也是深入理解C#的作者所写,可以收藏此网站以便更快的获取相关信息。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: