常见单例模式深度解析

在C#中实现单例模式
介绍
单例模式是软件工程中最著名的模式之一。本质上,单例是仅允许创建其自身的单个实例的类,并且通常提供对该实例的简单访问。最常见的情况是,单例在创建实例时不允许指定任何参数-否则,对实例的第二次请求但参数不同可能会出现问题!(如果应该为具有相同参数的所有请求访问相同的实例,则使用工厂模式更为合适。)本文仅涉及不需要参数的情况。通常,单例的要求是延迟创建它们-即,直到首次需要该实例时才创建该实例。
在C#中有多种不同的方式来实现单例模式。在这里,我将以相反的优雅顺序介绍它们,从最常见的线程安全性开始,逐步发展为完全延迟加载,线程安全,简单且高性能的版本。
所有这些实现都有四个共同的特征,但是:
单个构造函数,私有且无参数。这样可以防止其他类实例化它(这将违反模式)。请注意,它还防止了子类化-如果一个单例可以被子类化一次,则可以被子类化两次,并且如果每个子类都可以创建一个实例,则将违反该模式。如果您需要一个基本类型的实例,则可以使用工厂模式,但是直到运行时才知道确切的类型。
该类是密封的。严格来说,由于上述几点,这是不必要的,但可以帮助JIT进行更多优化。
一个静态变量,其中包含对创建的单个实例的引用(如果有)。
公共静态方法是获取对创建的单个实例的引用,并在必要时创建一个实例。
请注意,所有这些实现也都使用公共静态属性Instance 作为访问实例的方式。在所有情况下,都可以轻松地将属性转换为方法,而不会影响线程安全性或性能。
第一版-不是线程安全的

// Bad code! Do not use!
    public sealed class Singleton
    {
    private static Singleton instance=null;
    private Singleton()
    {
    }
    public static Singleton Instance
    {
    get
    {
    if (instance==null)
    {
    instance = new Singleton();
    }
    return instance;
    }
    }
    }

如前所述,以上内容不是线程安全的。两个不同的线程都可以评估测试if (instance==null)并发现测试是正确的,然后都创建实例,这违反了单例模式。请注意,实际上,在计算表达式之前可能已经创建了实例,但是内存模型不能保证实例的新值将被其他线程看到,除非已传递适当的内存屏障。
第二版-简单的线程安全

public sealed class Singleton
    {
    private static Singleton instance = null;
    private static readonly object padlock = new object();
    Singleton()
    {
    }
    public static Singleton Instance
    {
    get
    {
    lock (padlock)
    {
    if (instance == null)
    {
    instance = new Singleton();
    }
    return instance;
    }
    }
    }
    }

此实现是线程安全的。该线程在共享对象上获取一个锁,然后在创建实例之前检查是否已创建该实例。这可以解决内存屏障问题(因为锁定可确保所有读取均在获取锁之后逻辑发生,而解锁可确保所有写入均在锁释放之前逻辑发生)并确保只有一个线程将创建一个实例(仅一个线程一次可以位于代码的该部分中-到第二个线程进入该线程时,第一个线程将创建该实例,因此该表达式的计算结果为false)。不幸的是,每次请求实例时都需要获取锁,因此性能会受到影响。
请注意typeof(Singleton),我没有锁定此实现的某些版本,而是锁定了该类私有的静态变量的值。锁定其他类可以访问和锁定的对象(例如类型)可能会导致性能问题甚至死锁。这是我的一般样式偏好设置-尽可能仅锁定专门为锁定目的而创建的对象,或者出于特定目的(例如,等待/发出队列)而将其锁定在哪个文档上。通常,此类对象应为所使用的类专用。这有助于使编写线程安全的应用程序变得更加容易。
第三版-使用双重检查锁定尝试线程安全

```csharp
public sealed class Singleton
    {
    private static Singleton instance = null;
    private static readonly object padlock = new object();
    Singleton()
    {
    }
    public static Singleton Instance
    {
    get
    {
    if (instance == null)
    {
    lock (padlock)
    {
    if (instance == null)
    {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }
    }

此实现尝试是线程安全的,而不必每次都取出锁。不幸的是,该模式有四个缺点:
它在Java中不起作用。评论这似乎有些奇怪,但是值得一提的是,您是否需要Java中的单例模式,C#程序员也很可能是Java程序员。Java内存模型不能确保在将对新对象的引用分配给实例之前,构造函数已完成。Java内存模型针对1.5版进行了重新加工,但是在此之后,在没有易失性变量的情况下,双重检查锁定仍然无效(如C#)。
没有任何内存障碍,它在ECMA CLI规范中也被打破。在.NET 2.0内存模型(比ECMA规范更强)下,它很可能是安全的,但我宁愿不依赖那些更强的语义,尤其是在对安全性有任何疑问的情况下。将instance变量设置为volatile可以使其工作,就像显式的内存屏障调用一样,尽管在后一种情况下,即使专家也无法确切地确定需要哪些屏障。我倾向于尝试避免专家不同意对错的情况!
很容易出错。该模式必须与上面的完全一样-任何重大更改都可能影响性能或正确性。
它的性能仍然不如后来的实现。
第四版-并不那么懒,但是在不使用锁的情况下是线程安全的


```csharp

```csharp
public sealed class Singleton
    {
    private static readonly Singleton instance = new Singleton();
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }
    private Singleton()
    {
    }
    public static Singleton Instance
    {
    get
    {
    return instance;
    }
    }
    }

如您所见,这确实非常简单-但是为什么它是线程安全的,它有多懒呢?好吧,C#中的静态构造函数被指定为仅在创建类的实例或引用静态成员时执行,并且每个AppDomain仅执行一次。鉴于无论其他情况如何都需要执行对新构造类型的检查,因此比前面的示例中添加额外的检查要快。但是,有一些皱纹:
它没有其他实现那么懒。特别是,如果您拥有以外的静态成员Instance,则对这些成员的首次引用将涉及创建实例。在下一个实现中将对此进行纠正。
如果一个静态构造函数调用另一个而又再次调用第一个静态构造函数,则会带来复杂性。请查阅.NET规范(当前位于分区II的9.5.3节),以获取有关类型初始值设定项的确切性质的更多详细信息-它们不太可能咬住您,但是值得一提的是,静态构造函数会引用每个类型的初始值其他在一个周期中。
只有当类型未使用称为的特殊标志标记时,.NET才能保证类型初始值设定项的惰性beforefieldinit。不幸的是,C#编译器(至少在.NET 1.1运行时中提供)将所有没有静态构造函数(即,看起来像构造函数但被标记为静态的块)的类型都标记为beforefieldinit。我现在有一篇 文章,详细介绍了这个问题 。还要注意,它会影响性能,如页面底部所述。
使用此实现(仅此实现)可以采取的一种捷径是仅创建 instance一个公共静态只读变量,并完全摆脱该属性。这使得基本的骨架代码绝对很小!但是,许多人更喜欢拥有属性,以防将来需要采取进一步的措施,并且JIT内联可能会使性能相同。(请注意,如果您需要懒惰,则仍然需要静态构造函数本身。)
第五版-完全延迟实例化

public sealed class Singleton
    {
    private Singleton()
    {
    }
    public static Singleton Instance { get { return Nested.instance; } }
    private class Nested
    {
    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Nested()
    {
    }
    internal static readonly Singleton instance = new Singleton();
    }
    }

在此,实例化是由对嵌套类的静态成员的第一次引用触发的,该引用仅在中出现Instance。这意味着实现是完全懒惰的,但是具有以前实现的所有性能优势。请注意,尽管嵌套类可以访问封闭类的私有成员,但事实并非如此,因此需要instance在内部进行嵌套。但是,由于类本身是私有的,所以这不会引起任何其他问题。但是,为了使实例化变得懒惰,代码有些复杂。
第六版-使用.NET 4的Lazy类型
如果使用的是.NET 4(或更高版本),则可以使用System.Lazy 类型使懒惰变得非常简单。您需要做的就是将委托传递给构造函数,该构造函数调用Singleton构造函数-使用lambda表达式最容易完成。

public sealed class Singleton
    {
    private static readonly Lazy<Singleton>
        lazy =
        new Lazy<Singleton>
            (() => new Singleton());
            public static Singleton Instance { get { return lazy.Value; } }
            private Singleton()
            {
            }
            }

它很简单,性能也很好。 如果需要,还可以使用IsValueCreated属性检查实例是否已创建。
上面的代码隐式地LazyThreadSafetyMode.ExecutionAndPublication用作的线程安全模式Lazy。根据您的要求,您可能希望尝试其他模式。
性能与懒惰
在许多情况下,您实际上并不需要完全的惰性-除非您的类初始化做一些特别耗时的操作,或者在其他地方有副作用,否则可以忽略上面显示的显式静态构造函数。这可以提高性能,因为它允许JIT编译器进行一次检查(例如,在方法开始时进行检查),以确保类型已初始化,然后从此开始进行假设。如果您的单例实例是在相对紧凑的循环中引用的,则这可能会(相对)产生明显的性能差异。您应该确定是否需要完全延迟的实例化,并在类中适当地记录此决定。
该页面存在的很多原因是人们试图变得聪明,因此提出了双重检查的锁定算法。锁的态度是昂贵的,这是普遍的并且被误导了。我编写了一个非常快速的基准测试,它以十亿种方式尝试各种变体,以循环方式获取单例实例。这并不是十分科学,因为在现实生活中,您可能想知道,如果每次迭代实际上都涉及到对获取单例的方法的调用等,该过程有多快。但是,这确实显示了重要的意义。在我的笔记本电脑上,最慢的解决方案(约为5倍)是锁定解决方案(解决方案2)。那重要吗?当您牢记它仍然设法收购了十亿美元的单身人士时,可能不会时间不到40秒。(注意:本文最初是在很早以前写的-我希望现在可以有更好的性能。)这意味着,如果您“仅”每秒获取40万次单例,则获取的成本将不断增加达到1%的性能-因此,改善性能并不会起到太大作用。现在,如果您经常 获取单身人士-难道您不是在循环中使用它吗?如果您非常在乎提高性能,为什么不在循环之外声明局部变量,则获取一次单例然后循环。宾果游戏,即使是最慢的实现也很容易做到。
我将非常有兴趣看到一个现实世界的应用程序,在该应用程序中,使用简单锁定和使用较快速的解决方案之一之间的差异实际上带来了显着的性能差异。
例外情况
有时,您需要在单例构造函数中进行工作,这可能会引发异常,但对整个应用程序可能不会致命。潜在地,您的应用程序可能能够解决问题,并希望重试。在这个阶段,使用类型初始值设定项构造单例成为问题。不同的运行时对这种情况的处理方式不同,但是我不知道哪个运行者可以做所需的事情(再次运行类型初始化程序),即使这样做,您的代码也会在其他运行时中损坏。为了避免这些问题,我建议使用页面上列出的第二种模式-只需使用一个简单的锁,然后每次都进行检查,如果尚未成功构建该实例,则可以在方法/属性中进行构建。
感谢Andriy Tereshchenko提出了这个问题。
结论(2006年1月7日稍作修改; 2011年2月12日更新)
在C#中有多种不同的方式来实现单例模式。读者写信给我,详细介绍了他封装同步方面的一种方式,尽管我承认这在某些非常特殊的情况下(特别是在您想要非常高性能的情况下,并且能够确定单例是否已经被使用的能力)很有用。创建,并且完全懒惰(无论是否调用了其他静态成员)。我个人认为这种情况不会经常出现,值得在此页面上进行进一步介绍,但是如果您遇到这种情况,请 发邮件给我 。
我的个人偏爱是解决方案4:通常,我唯一会离开的地方是是否需要能够在不触发初始化的情况下调用其他静态方法,或者是否需要知道单例是否已被实例化。我不记得我上次遇到这种情况,假设我什至有过。在这种情况下,我可能会选择解决方案2,该解决方案仍然很不错,而且很容易就可以正确实现。
解决方案5很优雅,但比2或4棘手,而且正如我上面所说,它提供的好处似乎很少有用。如果您使用的是.NET 4,则解决方案6是实现懒惰的一种更简单的方法,它还具有明显的惰性。我目前倾向于仅通过习惯就使用解决方案4,但是如果我与经验不足的开发人员一起工作,我很可能会选择解决方案6作为一种简单且普遍适用的模式开始。
(我不会使用解决方案1,因为它已损坏,我不会使用解决方案3,因为它没有超过5的好处。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值