2

如果我想延迟实例化一个包含大量数据的类并且我希望它只有一个实例(单例),那么在设置实例之前是否需要仔细检查对象?(。网)。或者是否可以像在 AnotherWayForSingletonInstance 中那样只进行一次检查

这是代码:

class MyHeavyClass
{
private MyHeavyClass()
{
}

private static object _lock = new object();
private static MyHeavyClass _instance;
public static MyHeavyClass Instance{
    get{
        //check 1
        if (_instance == null)
        {
            lock(_lock)
            {
                //check 2
                if (_instance == null)
                {
                    _instance = new MyHeavyClass();
                }
            }
        }
        return _instance;
    }
}

public static MyHeavyClass AnotherWayForSingletonInstance{
    get{
        lock(_lock)
        {
            //check 1
            if (_instance == null)
            {
                _instance = new MyHeavyClass();
            }
        }
        return _instance;
    }
}

}

4

3 回答 3

2

永远不需要双重检查锁定 - 它纯粹是一种性能优化,依赖于平台相关的内存排序技巧来避免在大多数情况下锁定。单检查锁定总是足够的,更便携,更简单,所以你应该更喜欢它,除非基准测试表明你真的需要那个小提升。但还有一个更好的解决方案。

您应该完全避免显式锁定单例。相反,请尝试使用静态初始化程序,因为它们简单、(线程)安全、快速且延迟加载。

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;博士

不要对单例使用锁定,使用静态初始化器。

于 2013-02-26T23:15:37.237 回答
1

一次检查就可以了。

仅出于性能原因进行双重检查。由于锁只是为了防止多个线程同时创建实例,因此只有在您实际需要创建实例时才需要它。

于 2013-02-26T23:26:43.727 回答
0

这是我的答案:正如@Eamon 所建议的,解决对静态对象的线程访问的最佳方法是使用静态初始化程序初始化静态对象(在这种情况下,您不需要锁来访问实例)(另外,请检查Eamon 关于如何使用静态 ctor 的代码可以使实例在首次访问类之前不被分配)。

但是,如果您不能使用静态初始化程序,我认为使用双重检查锁定是有意义的,因为它可以提高性能。从我的测试中,我发现只有当我调用 Instance 方法超过 100,000 次时,性能才重要。

在我的代码以及 Eamon 的代码中,在足够大的迭代中,使用双重检查锁定可以显着提升性能。

这是我的测试代码(在 LinqPad 中运行)

void Main()
    {
        Stopwatch sw = Stopwatch.StartNew();
        const int loopSize = 10000000;
        for (int i = 0; i < loopSize; i++)
        {
            SingletonSingleLock o = SingletonSingleLock.Instance;
        }

        sw.ElapsedMilliseconds.Dump();


        sw = Stopwatch.StartNew();
        for (int i = 0; i < loopSize; i++)
        {
            SingletonDoubleLock o = SingletonDoubleLock.Instance;
        }
        sw.ElapsedMilliseconds.Dump();
    }

    /* Test results
        Elapsed milliseconds
        # of calls to Instance      1,000       10,000      100,000     1,000,000       10,000,000
        SingletonSingleLock             0           0           4           39              433
        SingletonDoubleLock             0           0           1           17              185
    */

    public class SingletonSingleLock
    {
        private SingletonSingleLock()
        {

        }

        private static object _lock = new object();
        private static SingletonSingleLock _instance;
        public static SingletonSingleLock Instance
        {
            get
            {
                lock(_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new SingletonSingleLock();
                    }
                }
                return _instance;
            }
        }

    }


    public class SingletonDoubleLock
    {
        private SingletonDoubleLock()
        {

        }

        private static object _lock = new object();
        private static SingletonDoubleLock _instance;

        public static SingletonDoubleLock Instance
        {
            get
            {
                if (_instance == null)
                {
                    lock(_lock)
                    {
                        if (_instance == null)
                        {
                            _instance = new SingletonDoubleLock();
                        }
                    }
                }
                return _instance;
            }
        }

    }

从 VS 分析器结果的屏幕截图中可以看出,双重检查锁定可以提高性能。 VS 性能结果

于 2013-02-27T16:29:04.603 回答