C# 单例模式的实现和性能对比

简介

单例指的是只能存在一个实例的类(在C#中,更准确的说法是在每一个AppDomain之中只能存在一个实例的类,它是软件工程中使用最多的几种模式之一。在第一个使用者建立了这个类的实例以后,其后须要使用这个类的就只能使用以前建立的实例,没法再建立一个新的实例。一般状况下,单例会在第一次被使用时建立。本文会对C#中几种单例的实现方式进行介绍,并分析它们之间的线程安全性和性能差别。程序员

单例的实现方式有不少种,但从最简单的实现(非延迟加载,非线程安全,效率低下),到可延迟加载,线程安全,且高效的实现,它们都有一些基本的共同点:安全

. 单例类都只有一个private的无参构造函数
. 类声明为sealed(不是必须的)
. 类中有一个静态变量保存着所建立的实例的引用
. 单例类会提供一个静态方法或属性来返回建立的实例的引用(eg.GetInstance)
复制代码

几种实现

一. 非线程安全

//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)而且建立两个不一样的instance,后建立的会替换掉新建立的,致使以前拿到的reference为空。并发

二. 简单的线程安全实现

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;
            }
        }
    }
}
复制代码

相比较于实现一,这个版本加上了一个对instance的锁,在调用instance以前要先对padlock上锁,这样就避免了实现一中的线程冲突,该实现自始至终只会建立一个instance了。可是,因为每次调用Instance都会使用到锁,而调用锁的开销较大,这个实现会有必定的性能损失。函数

注意这里咱们使用的是新建一个private的object实例padlock来实现锁操做,而不是直接对Singleton进行上锁。直接对类型上锁会出现潜在的风险,由于这个类型是public的,因此理论上它会在任何code里调用,直接对它上锁会致使性能问题,甚至会出现死锁状况。性能

Note: C#中,同一个线程是能够对一个object进行屡次上锁的,可是不一样线程之间若是同时上锁,就可能会出现线程等待,或者严重的会出现死锁状况。所以,咱们在使用lock时,尽可能选择类中的私有变量上锁,这样能够避免上述状况发生。测试

三. 双重验证的线程安全实现

public sealed calss 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;
        }
    } 
}
复制代码

在保证线程安全的同时,这个实现还避免了每次调用Instance都进行lock操做,这会节约必定的时间。spa

可是,这种实现也有它的缺点:线程

1. 没法在Java中工做。(具体缘由能够见原文,这边没怎么理解)
2. 程序员在本身实现时很容易出错。若是对这个模式的代码进行本身的修改,要倍加当心,由于double check的逻辑较为复杂,很容易出现思考不周而出错的状况。
复制代码

四. 不用锁的线程安全实现

public sealed class Singleton
{
    //在Singleton第一次被调用时会执行instance的初始化
    private static readonly Singleton instance = new Singleton();

    //Explicit static consturctor to tell C# compiler 
    //not to mark type as beforefieldinit
    static Singleton() {
    }

    private Singleton() {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}
复制代码

这个实现很简单,并无用到锁,可是它仍然是线程安全的。这里使用了一个static,readonly的Singleton实例,它会在Singleton第一次被调用的时候新建一个instance,这里新建时候的线程安全保障是由.NET直接控制的,咱们能够认为它是一个原子操做,而且在一个AppDomaing中它只会被建立一次。翻译

这种实现也有一些缺点:code

1. instance被建立的时机不明,任何对Singleton的调用都会提早建立instance
2. static构造函数的循环调用。若有A,B两个类,A的静态构造函数中调用了B,而B的静态构造函数中又调用了A,这两个就会造成一个循环调用,严重的会致使程序崩溃。
3. 咱们须要手动添加Singleton的静态构造函数来确保Singleton类型不会被自动加上beforefieldinit这个Attribute,以此来确保instance会在第一次调用Singleton时才被建立。
4. readonly的属性没法在运行时改变,若是咱们须要在程序运行时dispose这个instance再从新建立一个新的instance,这种实现方法就没法知足。
复制代码

五. 彻底延迟加载实现(fully lazy instantiation)

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的get方法里面调用,且只会在第一次调用前初始化。它是实现四的确保延迟加载的版本。

六 使用.NET4的Lazy类型

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() {
    }
}
复制代码

.NET4或以上的版本支持Lazy来实现延迟加载,它用最简洁的代码保证了单例的线程安全和延迟加载特性。

性能差别

以前的实现中,咱们都在强调代码的线程安全性和延迟加载。然而在实际使用中,若是你的单例类的初始化不是一个很耗时的操做或者初始化顺序不会致使bug,延迟初始化是一个无关紧要的特性,由于初始化所占用的时间是能够忽略不计的。

在实际使用场景中,若是你的单例实例会被频繁得调用(如在一个循环中),那么为了保证线程安全而带来的性能消耗是更值得关注的地方。

为了比较这几种实现的性能,我作了一个小测试,循环拿这些实现中的单例9亿次,每次调用instance的方法执行一个count++操做,每隔一百万输出一次,运行环境是MBP上的Visual Studio for Mac。结果以下:

线程安全性 延迟加载 测试运行时间(ms)
实现一 15532
实现二 45803
实现三 15953
实现四 不彻底 14572
实现五 14295
实现六 22875

测试方法并不严谨,可是仍然能够看出,方法二因为每次都须要调用lock,是最耗时的,几乎是其余几个的三倍。排第二的则是使用.NET Lazy类型的实现,比其余多了二分之一左右。其他的四个,则没有明显区别。

总结

整体来讲,上面说的多种单例实现方式在现今的计算机性能下差距都不大,除非你须要特别大并发量的调用instance,才会须要去考虑锁的性能问题。

对于通常的开发者来讲,使用方法二或者方法六来实现单例已是足够好的了,方法四和五则须要对C#运行流程有一个较好的认识,而且实现时须要掌握必定技巧,而且他们节省的时间仍然是有限的。

引用

本文大部分是翻译自Implementing the Singleton Pattern in C#,加上了一部分本身的理解。这是我搜索static readonly field initializer vs static constructor initialization时看到的,在这里对两位做者表示感谢。

相关文章
相关标签/搜索