网站首页 文章专栏 C# 线程安全的单例模式
C# 线程安全的单例模式
发布 作者:被打断de狗腿 浏览量:481
从最常见的非线程安全的开始介绍,到完全延迟加载、线程安全、简单且高性能的版本。

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

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

  • 一个构造函数,它是私有且无参数的。这可以防止其他类实例化它(这将违反单例模式的初衷)。请注意,它还可以防止子类化(subclassing)—— 一个单例可以被子类化一次,那么它就可以被子类化两次,如果这些子类中的都可以创建一个实例,那么就违反了该模式。如果你需要一个基本类型的单个实例,那应该使用工厂模式,直到运行时才知道确切的类型。
  • class 是密封的。严格来说,由于上述原因,这是没有必要的,但可以有助于 JIT 进行更多优化。
  • 一个静态变量,它应该是这个单例的唯一引用,如果有的话。
  • 一个调用这个单例公共静态方法或者静态属性(private set Property)。

请注意,就像上面提到的,使用公共的静态属性 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 的,然后都创建实例,这就违反了单例模式。请注意,事实上,在判断表达式之前可能已经创建了实例,但是内存模型不保证实例的新值会被其他线程看到,除非有合适的内存屏障。

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

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) ,而是锁定类私有的静态变量的值。锁定其他类可以访问和锁定的对象,这样可能会导致性能问题甚至死锁。 —— 只要有可能,就应该创建一个特定的对象,专门为这个锁所使用的对象。这些对象应该是这个类的私有对象,这有助于使编写线程安全的应用程序变得更加容易。

第三个版本 - 使用双重检查锁定,来实现线程安全

// 同1,垃圾代码
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;
        }
    }
}

这种方式是线程安全的,而且不需要每次调用都会被锁。不过,这种模式有四个缺点:

  1. 它在 Java 中不起作用。这TM是一件奇怪的事情了,但如果你需要 Java 中的单例模式,那就应该了解,并且很多人C#、Java都写的。在新的实例化时,Java的内存模型不能确保上一个实例化时的构造函数已经执行完毕。
  2. 没有任何内存屏障,它在 ECMA CLI 规范中也被打破了。在 .NET 2.0 内存模型(比 ECMA 规范更强)下它可能是安全的,但我宁愿不依赖那些更强大的语义,特别是对此有疑虑的时候。
  3. 很容易出错。这个模式需要与上面那些几乎相同 - 任何更改都可能影响性能或预期。
  4. 它的性能仍然不如后面的实现。

第四个版本 - 弱延迟加载,而且在不使用锁的情况下是线程安全的

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

    // 告诉编译器有明确的静态构造函数
    // 不将这个类型标记为 beforefieldinit
    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}

这时一个非常简单的实现。但是为什么它是线程安全的?C# 中的静态构造函数仅在创建类的实例或引用静态成员时执行,并且应用程序只会仅执行一次。单例模式无论在什么情况下都会去检查实例是否为 null,那么这将比在前面的示例中的检查要快。然而,还有一些小问题:

  • 它不像其他实现那样延迟。比如你有 Instance 以外的静态成员,对这些静态成员的第一次引用会创建实例,不过在第五版实现中会解决。
  • 如果静态函数互相调用,问题就会复杂化。
  • 类型不能被标记为 beforefieldinit ,.NET Framework 1.1 及以上版本才有静态构造函数

这个实现时可以使用的,不过得把 instance 改为公共只读的静态变量,这会让代码非常简洁。然而,许多人更喜欢只用一个属性来调用,以备之后的更改,而且 JIT 内联可能会使性能相同。(请注意,如果您需要延迟加载,静态构造函数本身仍然是必需的。)

第五版——完全惰性实例化

public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance { get { return Nested.instance; } }

    private class Nested
    {
        // 告诉编译器有明确的静态构造函数
        // 不将这个类型标记为 beforefieldinit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}

在这里,实例化是由对嵌套类的静态成员的第一个引用触发的,它只发生在这个实例的内部。 这意味着这个实现是完全延迟加载的,但具有以前的所有性能优势。请注意,尽管嵌套类可以访问封闭类的私有成员,但反之则不然,因此需要 instance 的访问修饰符改为 internal 。当然这不会有其他问题,因为这个内部类本身是私有的。这样做,代码和第四版比就闲的复杂了。

第六版 - 使用 .NET Framework 4 的 Lazy<T>类型

如果你使用的是 .NET Framework 4(或更高版本),则可以使用System.Lazy<T> 类型使延迟加载变得非常简单。所需要做的就是将委托传递给调用 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 编译器进行单向检查(例如在方法的开头)以确保类型已被初始化。如果单例实例在相对密集的循环中被引用,这可能会产生(相对)显着的性能差异。你应该自己决定是否需要完全延迟加载实例化。

例外

有时,在单例模式运行中能会引发异常,在不太严重时,程序能够解决问题并重试。在这个阶段使用类型初始化器来构造单例就会有问题。不同的运行时以不同的方式处理这种情况,但我不知道有哪种运行时能达到我的预期(再次运行类型初始化器),即使有,代码也会在其他运行时被破坏。为了避免这些问题,我建议使用页面上列出的第二种模式 - 只需使用一个简单的锁,并且每次都判断,如果还没有被实例化,那么就在方法/属性中构建实例。

loading