2013年02月05日 · 151 次访问

单例模式以及在C#中的使用

整理一份公司培训时候的幻灯片,这是由我所作,关于设计模式中单例模式以及C#编程语言中的使用情况,您也可以直接下载PDF版本

下面做一些简要的说明。

单例模式(Singleton Pattern),又称作单件模式,当然也有一种诙谐的称谓:单身模式。在经典的GoF所著的《Design Patterns》一书中,对单例模式有着详尽的介绍,这本书网上有全文版本。

单例模式的意图是保证一个类仅有一个实例,并且要提供一个全局访问点来访问这个实例。通常这个全局访问点是一个静态方法或者C#中的一个属性。

在C#中,典型的单例模式实现方法可以如下:

public class Manager
{
    private static Manager Mgr;

    //constructor must be private
    private Manager() { }

    //public and static, or you can change it to Property instead of Method.
    public static Manager GetInstance()
    {
        if (Mgr == null)
        {
            Mgr = new Manager();
        }
        return Mgr;
    }
}

当外部需要Manager的实例时,可以调用GetInstance()这个静态方法。由于Manager类的构造器是私有的,这也就避免了其他方式实例化这个Manager类。GetInstance()方法内部的实现,保证了全局中只有一个Manager实例。

问题肯定不会这么简单就被解决,比如在多线程环境中,上述代码就会有很大的隐患。

有一种情况很常见:两个线程同时调用GetInstance()方法; 当某一个线程由于Mgrnull而进入条件判断代码块的时候,而恰恰还没有执行实例化一个Manager对象,这时候另一个线程由于Mgrnull,所以也会进入这个条件语句中

上面两种情况,很显然都会创建Manager实例,这也就违背了单例模式的意图了。

利用C#的特性,我们可以把一个线程先锁住,等到这个线程完成后,再让下一个线程访问GetInstance()方法:

private static readonly object syncObject = new object();

//public and static, or you can change it to Property instead of Method.
public static Manager GetInstance()
{
    //double check
    if (Mgr == null)
    {
        lock (syncObject)
        {
            if (Mgr == null)
            {
                Mgr = new Manager();
            }
        }
    }
    return Mgr;
}

代码中用到了双重检查锁定(double check locking)的技术,是为了提高性能考虑,因为C#中lock语句是很耗性能的。第一道检查,是基于如果Mgr不为null的时候就不需要lock了,提高性能。第二道检查,是基于两个线程同时通过第一道检查后,第一个线程解锁后,由于Mgr此时已经不为null,所以第二个线程就不用实例化Manager了。

单例模式有两种实现方式,主要基于构建的方式不同:

  • 延迟初始化(Lazy Initialization),也叫“懒汉模式”:单例实例在第一次使用时被构建;
  • 热初始化(Eager Initialization),也叫“恶汉模式”:单例实例在类加载时创建 前面创建单例模式的方式都属于延迟初始化。.NET 4.0以后提供了一个Lazy<T>泛型类,可以被应用于这个场景,省却代码的编写量。
public class Manager
{
    private static Lazy<Manager> mgr = new Lazy<Manager>();
    
    //version of Thread Safe
    //private static Lazy<Manager> mgr = new Lazy<Manager>(true);
    
    private Manager() { }

    public static Manager GetInstance()
    {
        return mgr.Value;
    }
}

Lazy<T>的构造器重载版本可以帮我们解决多线程的问题。

C#使用静态初始化来完成单例模式中的热初始化。需要注意的是,不需要考虑多线程的问题,因为CLR会自动解决多线程同步的问题。如果程序经常要用到这个实例,运用热初始化可以显著提高性能。

public class Manager
{
    private static readonly Manager mgr = new Manager();

    private Manager() { }

    public static Manager GetInstance()
    {
        return mgr;
    }
}

StackOverflow中对于单例模式都是持否定态度的,比如这个

In theory: when you need to restrict the instantiation of an object to one instance. In practice: never.

主要基于下面几个原因:

  • 违反了单一职责原则
  • 耦合度过大
  • 单元测试基本无法进行
  • 开发混淆,造成混乱。比如作为API提供的时候。

总之,

There‘s at most a can use but there no need.