在C#中实现单例模式

介绍

单例模式是软件工程中最著名的模式之一。本质上单例是一个类,他只允许创建自身的单个实例,并且通常提供对该实例的简单访问。最常见的是,单例在创建实例时不允许指定任何参数-否则对实例的第二次请求但使用不同的参数可能会出现问题!(如果所有带有相同参数的请求都应该访问同一个实例,那么工厂模式更适合。)本文只处理不需要参数的情况。通常,单例的要求是它们懒惰地创建的–即,在第一次需要之前不会创建实例。

在C#中实现单例模式有很多种不同的方式。从最常见的非线程安全版本开始,到完全延迟加载、线程安全、简单且高性能的版本。

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

  • 单个构造函数,它是私有且无参数的。这可以防止其他类实例化(这将违反模式)。请注意,它还可以防止派生类实例化–如果一个单例可以被派生类实例化一次,,它可以被派生类实例化两次,如果每个子类都可以创建实例化,则违反了该模式。如果你需要基类的单个实例,则可以使用工厂模式,但直到运行时才知道确切的类型。
  • 该类是密封的,由于上诉原因,严格来说这是不必要的,但可能有助于JIT进一步优化。
  • 一个静态变量,它保存对单个创建的实例的引用,如果有的话。
  • 一种获取对单个创建的实例的引用的公共静态方法,必要时创建一个。

第一个版本——非线程安全版本——懒汉式

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,然后创建两个实例,这违反了单例模式。实际上,在计算表达式之前可能已经创建了实例,但是内存模型不保证其他线程可以看到实例的新值,除非已经传递了合适的内存屏障。

第二个版本——通过锁定实现简单的线程安全——懒汉式

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规范(目前是分区||的第9.5.3节),了解有关类型初始化器的确切性质的更多详细信息——它们不太可能会影响你,但是有必要了解静态构造函数在循环中相互引用的后果。

  • 类型初始化器的懒惰性只有在.NET没有使用名BeforeFieldInit的特殊标志标记类型时才能得到保证。不幸的是,C#编译器(至少在.NET1.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类型

如果你使用的是.NET4(或更高版本),则可以使用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.ExecutionAndPubliccation用作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。)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值