网站首页 文章专栏 C# 线程安全的单例模式
从最常见的非线程安全的开始介绍,到完全延迟加载、线程安全、简单且高性能的版本。
在 C# 中实现单例模式有多种不同的方法。从最常见的非线程安全的开始,到完全延迟加载、线程安全、简单且高性能的版本。
所有这些实现都有四个共同特征:
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;
}
}
}
这种方式是线程安全的,而且不需要每次调用都会被锁。不过,这种模式有四个缺点:
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
。当然这不会有其他问题,因为这个内部类本身是私有的。这样做,代码和第四版比就闲的复杂了。
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 编译器进行单向检查(例如在方法的开头)以确保类型已被初始化。如果单例实例在相对密集的循环中被引用,这可能会产生(相对)显着的性能差异。你应该自己决定是否需要完全延迟加载实例化。
有时,在单例模式运行中能会引发异常,在不太严重时,程序能够解决问题并重试。在这个阶段使用类型初始化器来构造单例就会有问题。不同的运行时以不同的方式处理这种情况,但我不知道有哪种运行时能达到我的预期(再次运行类型初始化器),即使有,代码也会在其他运行时被破坏。为了避免这些问题,我建议使用页面上列出的第二种模式 - 只需使用一个简单的锁,并且每次都判断,如果还没有被实例化,那么就在方法/属性中构建实例。