单例的简单实现

本文关键字:实现 简单 单例 | 更新日期: 2023-09-27 17:56:18

这不是

实现单例而不是双重检查锁定曼波-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

单例的简单实现

如果你的单例有多次初始化自己的危险,那么你会遇到更糟糕的问题。为什么不直接使用:

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

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

编辑:如果我不清楚,你的代码是可怕的错误if检查和new不是线程安全的!您需要使用适当的单例类。

您很可能正在创建多个实例,但这些实例将被垃圾回收,因为它们不在任何地方使用。在任何情况下,静态 _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;
        }
    }
}

正如我们所看到的,在上面的示例中,方法以创建丢弃对象为代价是无锁定的。在任何情况下,对于此类 API 的使用者,Value 属性都不会更改。

平衡权衡取舍

Lock Freedom是有代价的,并且是仔细选择权衡的问题。在这种情况下,锁定自由的代价是您必须创建不打算使用的对象的实例。这可能是一个可以接受的代价,因为您知道通过无锁,死锁和线程争用的风险更低。

然而,在这种特殊情况下,单例的语义本质上是创建对象的单个实例,所以我宁愿选择Lazy<T>,正如@Centro在他的回答中引用的那样。

尽管如此,它仍然引出了一个问题,我们什么时候应该使用Interlocked.CompareExchange?我喜欢你的例子,它发人深省,当它不像引号那样严重错误时,许多人很快就会将其diss为错误@Blindy。

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

    生成
  • 一个且仅生成一个实例有多重要?
  • 无锁有多重要?

只要您意识到权衡,并有意识地决定创建新对象以利于无锁,那么您的示例也可能是一个可以接受的答案。

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

public class Singleton
{
    private static Singleton _instance = new Singleton();
    private Singleton() {}
    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }
}
我不

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

这个呢?

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

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

您的单一实例初始值设定项的行为完全符合预期。参见 Raymond Chen 的无锁算法: 单例构造函数:

这是一个双重检查锁,但没有锁定。在进行初始构造时,我们没有锁定,而是让它成为谁可以创建对象的自由竞争。如果五个线程同时访问此代码,当然,让我们创建五个对象。在每个人都创建了他们认为是获胜的对象之后,他们调用InterlockedCompareExchangePointerRelease来尝试更新全局指针。

当可以让多个线程尝试创建单例(并让所有失败者销毁其副本)时,此技术适用。如果创建单例成本高昂或有不必要的副作用,那么您不想使用免费算法。

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

奖金阅读

  • 一次性初始化帮助程序函数使您不必自己编写所有这些代码。它们处理所有同步和内存屏障问题,并支持单人初始化和免费初始化模型。
  • .NET 的延迟初始化原语提供了相同的 C# 版本。

这不是线程安全的。

您需要一把锁来将if()Interlocked.CompareExchange()固定在一起,然后您就不再需要CompareExchange了。

您仍然遇到一个问题,即很可能正在创建和丢弃单例实例。执行 Interlocked.CompareExchange() 时,无论赋值是否成功,都将始终执行 Singleton 构造函数。所以你不会比你说更好(或更糟,恕我直言):

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

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

.NET 的明显单例实现?

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

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

我认为.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在这里有一篇不错的文章,涵盖了实现单例的许多方法以及每种方法的问题。

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

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

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