Singleton的简单实现

时间:2011-05-13 19:20:33

标签: c# .net multithreading singleton

这不是一种更简单,更安全(也就是更好)的方法来实现单例而不是双重检查锁定mambo-jambo吗?这种方法有什么缺点吗?


public class Singleton
{
    private static Singleton _instance;
    private Singleton() { Console.WriteLine("Instance created"); }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                Interlocked.CompareExchange(ref _instance, new Singleton(), null);
            }
            return _instance;
        }
    }
    public void DoStuff() { }
}

编辑:线程安全测试失败,任何人都可以解释原因吗?为什么Interlocked.CompareExchange不是真正的原子?


public class Program
{
   static void Main(string[] args)
   {
      Parallel.For(0, 1000000, delegate(int i) { Singleton.Instance.DoStuff(); });
   }
} 

Result (4 cores, 4 logical processors)
Instance created
Instance created
Instance created
Instance created
Instance created

13 个答案:

答案 0 :(得分:10)

如果您的单身人士有多次初始化的危险,那么您的问题会更严重。为什么不直接使用:

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

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

在初始化方面绝对是线程安全的。

编辑:如果我不清楚,你的代码非常 错误 if检查和new 线程安全!你需要使用一个合适的单例类。

答案 1 :(得分:8)

您可能正在创建多个实例,但这些实例会被垃圾收集,因为它们不会在任何地方使用。在任何情况下,静态_instance字段变量都不会多次更改其值,即从null变为有效值的单次时间。因此,尽管已经创建了多个实例,但此代码的使用者将只看到相同的实例。

锁定免费编程

Joe Duffy,在他的名为 Windows上的并发编程的书中实际上分析了你试图在第10章,内存模型和锁定自由,第526页上使用的这种模式。

他将此模式称为松散引用的延迟初始化:

public class LazyInitRelaxedRef<T> where T : class
{
    private volatile T m_value;
    private Func<T> m_factory;

    public LazyInitRelaxedRef(Func<T> factory) { m_factory = factory; }


    public T Value
    {
        get
        {
            if (m_value == null) 
              Interlocked.CompareExchange(ref m_value, m_factory(), null);
            return m_value;
        }
    }

    /// <summary>
    /// An alternative version of the above Value accessor that disposes
    /// of garbage if it loses the race to publish a new value.  (Page 527.)
    /// </summary>
    public T ValueWithDisposalOfGarbage
    {
        get
        {
            if (m_value == null)
            {
                T obj = m_factory();
                if (Interlocked.CompareExchange(ref m_value, obj, null) != null && obj is IDisposable)
                    ((IDisposable)obj).Dispose();
            }
            return m_value;
        }
    }
}

正如我们所看到的,在上面的示例中,方法是免费锁定的,其代价是创建一次性对象。在任何情况下,Value属性都不会因此类API的使用者而改变。

平衡权衡

锁定自由需要付出代价,并且需要谨慎选择权衡取舍。在这种情况下,锁定自由的代价是您必须创建不会使用的对象实例。这可能是一个可接受的支付价格,因为你知道通过免于锁定,死锁风险和线程争用的风险较低。

在这个特定的实例中,单例的语义本质上是创建一个对象的单个实例,所以我更愿意选择{{3正如@Centro在他的回答中引用的那样。

然而,当 我们使用Interlocked.CompareExchange时,它仍然存在问题?我喜欢你的例子,这是非常发人深省的,当@Blindy引用它不是可怕错误时,许多人很快就把它误解为错误。

这一切归结为你是否计算了权衡并决定:

  • 您制作一个且只有一个实例有多重要?
  • 锁定免费有多重要?

只要你意识到权衡取舍,并且有意识地决定创建新对象以获得无锁,那么你的例子也可以是一个可以接受的答案。

答案 2 :(得分:6)

为了不使用'双重检查锁定mambo-jambo'或者根本不使用自己的单例重新发明轮子,请使用.NET 4.0中包含的现成解决方案 - Lazy<T>

答案 3 :(得分:3)

public class Singleton
{
    private static Singleton _instance = new Singleton();
    private Singleton() {}

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

答案 4 :(得分:3)

我不相信你完全可以相信。是的,Interlocked.CompareExchanger是原子的,但是新的Singleton()在任何非平凡的情况下都不会是原子的。由于在交换值之前必须进行评估,因此这通常不是线程安全的实现。

答案 5 :(得分:3)

这是怎么回事?

public sealed class Singleton
{
    Singleton()
    {
    }

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

    class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

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

这是此页面上的第五个版本: http://www.yoda.arachsys.com/csharp/singleton.html

我不确定,但作者似乎认为它既是线程安全的又是延迟加载。

答案 6 :(得分:2)

您的单例初始化程序的行为完全符合预期。见Raymond Chen的Lock-free algorithms: The singleton constructor

  

这是一个双重检查锁,但没有锁定。在进行初始构建时,我们只是让它成为一个免费的人来创建对象,而不是锁定。如果五个线程同时到达此代码,那么,让我们创建五个对象。在每个人创建他们认为是获胜对象之后,他们调用InterlockedCompareExchangePointerRelease来尝试更新全局指针。

     

当可以让多个线程尝试创建单例(并让所有输家都销毁他们的副本)时,这种技术是合适的。如果创建单例是昂贵的或有不必要的副作用,那么你不想使用free-for-all算法。

每个线程创建对象;因为它认为还没有人创造它。但是在InterlockedCompareExchange期间,只有 一个 主题才能真正设置全局单例。

奖金阅读

答案 7 :(得分:1)

这不是线程安全的。

您需要锁定才能将if()Interlocked.CompareExchange()放在一起,然后您将不再需要CompareExchange

答案 8 :(得分:1)

你仍然有一个问题,你很可能正在创建和丢弃你的单身实例。执行Interlocked.CompareExchange()时,无论赋值是否成功,都将始终执行Singleton构造函数。因此,与你说的相比,你的情况并没有好转(或者更糟糕的是,恕我直言):

if ( _instance == null )
{
  lock(latch)
  {
    _instance = new Singleton() ;
  }
}

与线程争用相比,性能更好,而不是将lock和测试的位置交换为null,但存在构造额外实例的风险。

答案 9 :(得分:1)

答案 10 :(得分:1)

自动属性初始化(C#6.0)似乎不会导致您看到的Singleton的多个实例化。

public class Singleton
{    
    static public Singleton Instance { get; } = new Singleton();
    private Singleton();
}

答案 11 :(得分:0)

我认为.NET 4.0使用System.Lazy<T>之后最简单的方式:

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

    public static Singleton Instance { get { return lazy.Value; } }

    private Singleton() { }
}

Jon Skeet有一篇很好的文章here,涵盖了很多实现单例的方法以及每个方法的问题。

答案 12 :(得分:0)

不要使用锁定。使用您的语言环境

大多数简单的线程安全实现是:

public class Singleton
{
    private static readonly Singleton _instance;

    private Singleton() { }

    static Singleton()
    {
        _instance = new Singleton();
    }

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