您永远不需要双重检查锁定 - 它纯粹是一种性能优化,依赖于平台相关的内存排序技巧来避免在大多数情况下锁定。单检查锁定总是足够的,更便携,更简单,所以你应该更喜欢它,除非基准测试表明你真的需要那个小提升。但还有一个更好的解决方案。
您应该完全避免显式锁定单例。相反,请尝试使用静态初始化程序,因为它们简单、(线程)安全、快速且延迟加载。
sealed class MyHeavyClass
{
MyHeavyClass() {}
public static readonly MyHeavyClass Instance = new MyHeavyClass();
}
这样的实例不是在应用程序启动时创建的,而是在第一次使用类型或字段之前有点懒惰。确切的规则取决于是否存在静态构造函数,但如果您可以接受初始化程序有时比必要的更早执行,这很好。在 .NET v4 上,初始化非常懒惰。99% 的情况下,这应该是您的首选实现,因为它可能是最快、最简单的一种。即使在 .NET v3.5 和更早版本上,也不会加载,直到遇到引用该类型的方法。
这段代码比基于锁的版本更快,因为一旦类完全加载,访问字段时就不需要警卫或锁了。特别是,JIT 可以简单地假设变量已设置,理论上甚至可以省略空值检查和提升循环读取等内容。如果您确实需要精确控制延迟加载的时间;尝试更简单的 lock-then-check 而不是双重检查锁定(这依赖于一些棘手的内存模型细节)——但实际上,我怀疑几乎没有人需要这种精确的控制;你只是想避免不必要的工作。
关于双重检查锁定:据我所知,即使在 .NET 上,您也需要volatile
完全可移植双重检查锁定的关键字:.NET 的 ARM 实现没有与您相同的写入顺序保证重新习惯于 x86。即使它适用于 ARM 和各种单声道平台,如果它比简单的静态初始化程序慢,为什么还要使用如此复杂的实现呢?
基准测试结果
AlwaysLock 初始化
每次迭代 37.09 纳秒(1000000 次 AlwaysLock)
DoubleCheckedLocking 初始化
每次迭代 2.78 纳秒(1000000 次 DoubleCheckedLocking 迭代)
静态初始化器初始化
每次迭代 2.13 纳秒(StaticInitializer 的 1000000 次迭代)
静态构造函数初始化
每次迭代 2.56 纳秒(StaticConstructor 的 1000000 次迭代)
每次迭代 38.45 纳秒(10000000 次 AlwaysLock)
每次迭代 2.07 纳秒(10000000 次 DoubleCheckedLocking)
每次迭代 1.57 纳秒(StaticInitializer 的 10000000 次迭代)
每次迭代 1.57 纳秒(StaticConstructor 的 10000000 次迭代)
每次迭代 21.71 纳秒(10000000 次 AlwaysLock 同步迭代)
每次迭代 4.62 纳秒(DoubleCheckedLocking 的 10000000 次同步迭代)
每次迭代 3.15 纳秒(StaticInitializer 的 10000000 个同步迭代器)
每次迭代 3.17 纳秒(StaticConstructor 的 10000000 个同步迭代器)
基准代码
void Main()
{
const int loopSize = 10000000;
Bench(loopSize/10, ()=> AlwaysLock.Inst);
Bench(loopSize/10, ()=> DoubleCheckedLocking.Inst);
Bench(loopSize/10, ()=> StaticInitializer.Inst);
Bench(loopSize/10, ()=> StaticConstructor.Inst);
Console.WriteLine();
Bench(loopSize, ()=> AlwaysLock.Inst);
Bench(loopSize, ()=> DoubleCheckedLocking.Inst);
Bench(loopSize, ()=> StaticInitializer.Inst);
Bench(loopSize, ()=> StaticConstructor.Inst);
Console.WriteLine();
SBench(loopSize, ()=> AlwaysLock.Inst);
SBench(loopSize, ()=> DoubleCheckedLocking.Inst);
SBench(loopSize, ()=> StaticInitializer.Inst);
SBench(loopSize, ()=> StaticConstructor.Inst);
//uncommenting the next lines will cause instantiation of
//StaticInitializer but not StaticConstructor right before this method.
//var o = new object[]{
// StaticInitializer.Inst, StaticConstructor.Inst};
}
static void Bench<T>(int iter, Func<T> func) {
string name = func().GetType().Name;
var sw = Stopwatch.StartNew();
Parallel.For(0,iter,i=>func());
var sec = sw.Elapsed.TotalSeconds;
Console.Write("{0:f2} nanoseconds per iteration ({1} iters of {2})\n"
, sec*1000*1000*1000/iter, iter, name);
}
static void SBench<T>(int iter, Func<T> func) {
string name = func().GetType().Name;
var sw = Stopwatch.StartNew();
for(int i=0;i<iter;i++) func();
var sec = sw.Elapsed.TotalSeconds;
Console.Write("{0:f2} nanoseconds per iteration ({1} sync iters of {2})\n"
, sec*1000*1000*1000/iter, iter, name);
}
sealed class StaticInitializer {
StaticInitializer(){ Console.WriteLine("StaticInitializer init"); }
public static readonly StaticInitializer Inst = new StaticInitializer();
//no static constructor, initialization happens before
//the method with the first access
}
sealed class StaticConstructor {
StaticConstructor(){ Console.WriteLine("StaticConstructor init"); }
//a static constructor prevents initialization before the first access.
static StaticConstructor(){}
public static readonly StaticConstructor Inst = new StaticConstructor();
}
sealed class AlwaysLock {
AlwaysLock(){ Console.WriteLine("AlwaysLock init"); }
static readonly object _lock = new object();
static AlwaysLock _instance;
public static AlwaysLock Inst { get {
lock(_lock)
if (_instance == null)
_instance = new AlwaysLock();
return _instance;
} }
}
sealed class DoubleCheckedLocking {
DoubleCheckedLocking(){ Console.WriteLine("DoubleCheckedLocking init"); }
static readonly object _lock = new object();
static DoubleCheckedLocking _instance;
public static DoubleCheckedLocking Inst { get {
if (_instance == null)
lock(_lock)
if (_instance == null)
_instance = new DoubleCheckedLocking();
return _instance;
} }
}
TL;博士
不要对单例使用锁定,使用静态初始化器。