【译】在C#中实现单例模式

2020年11月23日 阅读数:17
这篇文章主要向大家介绍【译】在C#中实现单例模式,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

介绍

单例模式是软件工程中最着名的模式之一。从本质上讲,单例是一个只容许建立自身的单个实例的类,而且一般能够简单地访问该实例。最多见的是,单例不容许在建立实例时指定任何参数——不然对实例的第二个请求但具备不一样的参数可能会有问题!(若是对于具备相同参数的全部请求都应访问相同的实例,则工厂模式更合适。)本文仅处理不须要参数的状况。一般,单例的要求是它们是懒惰地建立的——即直到第一次须要时才建立实例。git

在C#中实现单例模式有各类不一样的方法。我将以优雅的相反顺序呈现它们,从最多见的、不是线程安全的版本开始,一直到彻底延迟加载的、线程安全的、简单且高性能的版本。程序员

然而,全部这些实现都有四个共同特征:github

  • 单个构造函数,它是私有且无参数的。这能够防止其余类实例化它(这将违反模式)。请注意,它还能够防止子类化——若是一个单例对象能够被子类化一次,那么它就能够被子类化两次,若是每一个子类均可以建立一个实例,则违反了该模式。若是您须要基类型的单个实例,则可使用工厂模式,可是确切的类型要到运行时才能知道。
  • 类是密封的。严格来讲,因为上述缘由,这是没必要要的,可是能够帮助JIT进行更多的优化。
  • 一个静态变量,用于保存对单个已建立实例的引用(若是有的话)。
  • 公共静态意味着获取对单个已建立实例的引用,必要时建立一个实例。

请注意,全部这些实现还使用公共静态属性Instance 做为访问实例的方法。在全部状况下,能够轻松地将属性转换为方法,而不会影响线程安全性或性能。算法

第一个版本 ——不是线程安全的

// 糟糕的代码!不要使用!
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)并发现它为true,而后两个都建立实例,这违反了单例模式。请注意,实际上,在计算表达式以前可能已经建立了实例,可是内存模型不保证其余线程能够看到实例的新值,除非已经传递了合适的内存屏障(互斥锁)。c#

第二个版本 —— 简单的线程安全

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),而是锁定了类私有的静态变量的值。锁定其余类能够访问和锁定的对象(例如类型)会致使性能问题甚至死锁。这是个人风格偏好——只要有可能,只锁定专门为锁定目的而建立的对象,或者为了特定目的(例如,等待/触发队列)而锁定的文档。一般这些对象应该是它们所使用的类的私有对象。这有助于使编写线程安全的应用程序变得更加容易。并发

第三个版本 - 使用双重检查锁定尝试线程安全

// 糟糕的代码!不要使用!
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内存模型没法确保构造函数在将新对象的引用分配给Instance以前完成。Java内存模型经历了1.5版本的从新改进,可是在没有volatile变量(如在C#中)的状况下,双重检查锁定仍然会被破坏。
  • 在没有任何内存障碍的状况下,ECMA CLI规范也打破了这一限制。有可能在.NET 2.0内存模型(比ECMA规范更强)下它是安全的,但我宁愿不依赖那些更强大的语义,特别是若是对安全性有任何疑问的话。使instance变量volatile变得有效,就像明确的内存屏障调用同样,尽管在后一种状况下,甚至专家也没法准确地就须要哪些屏障达成一致。我尽可能避免专家对对错意见也不一致的状况!
  • 这很容易出错。该模式须要彻底如上所述——任何重大变化均可能影响性能或正确性。
  • 它的性能仍然不如后续的实现。

第四个版本 - 不太懒,不使用锁且线程安全

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    // 显式静态构造函数告诉C#编译器
    // 不要将类型标记为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
    {
        // 显式静态构造告诉C#编译器
        // 未标记类型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<Singleton>的线程安全模式。根据您的要求,您可能但愿尝试其余模式。

性能与懒惰

在许多状况下,您实际上并不须要彻底懒惰——除非您的类初始化作了一些特别耗时的事情,或者在其余地方产生了一些反作用,不然最好忽略上面所示的显式静态构造函数。这能够提升性能,由于它容许JIT编译器进行一次检查(例如在方法的开头)以确保类型已经初始化,而后从那时开始设定它。若是在相对紧密的循环中引用单例实例,则会产生(相对)显著的性能差别。您应该决定是否须要彻底延迟实例化,并在类中适当地记录此决策。

这个页面存在的不少缘由是人们试图变得聪明,所以提出了双重检查锁定算法。咱们经常认为锁定是昂贵的,这被误导的。我写了一个很是快速的基准测试,在一个循环中获取10亿次单例实例,并尝试不一样的变体。这并非很科学,由于在现实生活中,您可能想知道若是每次迭代都涉及到对获取单例的方法的调用,那么速度有多快。然而这确实显示了一个重要的观点。在个人笔记本电脑上,最慢的解决方案(大约5倍)是锁定解决方案(解决方案2)。这很重要吗?可能不会,当您记住它仍然可以在40秒内获取10亿次Singleton。(注意:这篇文章最初是在好久之前写的——如今我但愿有更好的性能。)这意味着,若是你是“仅仅”每秒得到40万次单例实例,那么花费的成本将是1%的性能——因此不会作不少事情去改进它。如今,若是你常常 得到单例实例——你是否可能在循环中使用它?若是您很是关心如何提升性能,为何不在循环外声明一个局部变量,先获取一次Singleton,而后再循环呢。Bingo,即便是最慢的实现性能也足够了。

我很是有兴趣看到一个真实的应用程序,在这个应用程序中,使用简单锁定和使用一种更快的解决方案之间的差别实际上会带来显著的性能差别。

异常

有时,您须要在单例构造函数中执行一些操做,这可能会抛出异常,但可能不会对整个应用程序形成致命影响。您的应用程序可能可以解决此问题,并但愿再次尝试。在这个阶段,使用类型初始化器来构造单例会出现问题。不一样的运行时处理这种状况的方式不一样,但我不知道有哪些运行时执行了所需的操做(再次运行类型初始化程序),即便有一个运行时这样作,您的代码也会在其余运行时被破坏。为了不这些问题,我建议使用文章里列出的第二种模式 ——只需使用一个简单的锁,并每次都进行检查,若是还没有成功构建实例,则在方法/属性中构建实例。

结论

在C#中实现单例模式有各类不一样的方法。读者已经写信给我详细说明了他已经封装了同步方面的方法,虽然我认可这可能在一些很是特殊的状况下有用(特别是在你想要很是高性能的地方,以及肯定单例是否已经建立,并彻底懒惰,而不考虑其余静态成员被调用)。我我的并不认为这种状况会常常出现,值得在这篇文章中进一步改进,但若是你处于这种状况,请发邮件给我

个人我的的偏好是解决方案4:我一般惟一一次不采用它是由于我须要可以在不触发初始化的状况下调用其余静态方法,或者若是我须要知道单例是否已经被实例化。我不记得上次我处于那种状况是何时了,假设我有过,在那种状况下,我可能会选择解决方案2,这仍然是很好的,很容易正确实现。

解决方案5很优雅,可是比2或4更复杂,正如我上面所说,它提供的好处彷佛只是不多有用。解决方案6是一种更简单的方法来实现懒惰,若是你使用.NET 4.它还有一个优点,它显然是懒惰的。我目前仍然倾向于使用解决方案4,这仅仅是出于习惯——但若是我与没有经验的开发人员合做,我极可能会选择解决方案6做为一种简单且广泛适用的模式。

(我不会使用解决方案1,由于它是有缺陷的,我也不会使用解决方案3,由于它的好处没有超过5。)

原文做者:Jon Skeet

原文地址:Implementing the Singleton Pattern in C#

wif 项目代码:https://github.com/LeoYang-Chuese/wif