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

在 C# 中实现单例模式有多种方法,每种方法在线程安全性、性能特点和实现复杂度上各有不同。本文将详细介绍从最简单的非线程安全版本到完全延迟加载、线程安全且高性能的实现方案。

单例模式的核心特征

所有单例实现都应具备以下四个共同特征:

  • 私有无参构造函数:防止外部类实例化,确保单例的唯一性。同时也能防止子类化带来的潜在问题
  • 密封类(推荐):虽然不是绝对必要,但有助于 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;
        }
    }
}

问题:在多线程环境下,如果两个线程同时检查 instance == null 且都得到 true,则会创建多个实例,违反单例原则。

版本二:简单线程安全

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

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            lock (padlock)
            {
                if (instance == null)
                {
                    instance = new Singleton();
                }
                return instance;
            }
        }
    }
}

优点:通过锁机制确保线程安全 缺点:每次访问实例都需要获取锁,性能开销较大

注意:这里使用私有静态对象作为锁对象,避免了使用 typeof(Singleton) 可能导致的死锁问题。

版本三:双重检查锁定

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

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

优点:减少了锁的使用次数,提升性能 缺点

  1. 在 Java 中不可靠(内存模型差异)
  2. 依赖于特定的内存模型语义
  3. 实现容易出错
  4. 性能仍不如后续版本

版本四:静态初始化(非完全延迟加载)

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

    // 显式静态构造函数,防止 beforefieldinit 标记
    static Singleton() { }

    private Singleton() { }

    public static Singleton Instance => instance;
}

优点

  • 实现简单
  • 线程安全(由 CLR 保证)
  • 性能优秀

缺点:不是完全延迟加载,只要访问类的任何静态成员都会触发实例化

版本五:完全延迟加载

public sealed class Singleton
{
    private Singleton() { }

    public static Singleton Instance => Nested.instance;

    private class Nested
    {
        // 显式静态构造函数
        static Nested() { }

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

优点

  • 完全延迟加载
  • 线程安全
  • 高性能

缺点:实现相对复杂

版本六:使用 Lazy<T>(.NET Framework 4+)

public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazy = 
        new Lazy<Singleton>(() => new Singleton());

    public static Singleton Instance => lazy.Value;

    private Singleton() { }
}

优点

  • 实现简洁
  • 线程安全(默认使用 LazyThreadSafetyMode.ExecutionAndPublication
  • 完全延迟加载
  • 可通过 IsValueCreated 属性检查是否已实例化

性能与延迟加载的考量

在大多数情况下,完全延迟加载并非必需。如果初始化过程不涉及特别耗时的操作,使用静态初始化(版本四)可能更合适,因为它允许 JIT 编译器进行更好的优化。

异常处理考虑

如果单例构造函数可能抛出异常且需要恢复,建议使用版本二的简单线程安全实现。类型初始化器在遇到异常后的行为在不同运行时中可能不一致,而使用锁和显式检查的方式提供了更可靠的异常处理机制。

总结

版本线程安全延迟加载性能推荐度
版本一❌ 不推荐
版本二⭐️ 基础可用
版本三⭐️ 可用但有隐患
版本四⭐️⭐️ 推荐(非延迟场景)
版本五⭐️⭐️⭐️ 推荐
版本六⭐️⭐️⭐️⭐️ 强烈推荐(.NET 4+)

对于新项目,如果目标平台支持 .NET Framework 4 或更高版本,推荐使用 Lazy<T> 实现,它在简洁性、安全性和性能之间取得了最佳平衡。

loading