C#线程安全单例模式

109

我对单例模式有一些疑问,并参考了这里的文档: http://msdn.microsoft.com/en-us/library/ff650316.aspx

下面的代码是从文章中摘录出来的:

using System;

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

具体地说,在上面的例子中,有必要在加锁之前和之后两次将实例与null进行比较吗?这是必要的吗?为什么不先执行锁定操作然后再进行比较呢?

简化为以下方式是否存在问题?

   public static Singleton Instance
   {
      get 
      {
        lock (syncRoot) 
        {
           if (instance == null) 
              instance = new Singleton();
        }

         return instance;
      }
   }

执行锁定操作是否会很耗费资源?


26
顺便提一下,Jon Skeet在单例模式的线程安全方面有一篇精彩的文章:http://csharpindepth.com/Articles/General/Singleton.aspx - Arran
最好使用“lazy static init”... - Mitch Wheat
1
我在这里还有其他带说明的示例:http://csharpindepth.com/Articles/General/Singleton.aspx - Serge V.
Java世界中有完全相同的问题在这里 - RBT
11个回答

161

与简单的指针检查instance != null相比,执行锁操作非常昂贵。

您在此处看到的模式称为双重校验锁定。其目的是避免仅在第一次访问单例时才需要的昂贵锁操作。实现如此之因为它还必须确保初始化单例时没有出现线程竞争条件导致的错误。

这样想:不带lock的裸null检查仅在该答案为“是,对象已构建”时才能提供正确可用的答案。但是如果答案是“尚未构建”,则您没有足够的信息,因为您真正想知道的是“尚未构建且没有其他线程打算很快构建它”。因此,您使用外部检查作为非常快速的初始测试,并且只有当答案为“否”时,您才会启动适当的、无错误但“昂贵”的过程(锁定然后检查)。

上述实现对于大多数情况已足够,但此时最好去阅读Jon Skeet关于C#中的单例模式的文章,该文章还评估了其他替代方案。


双重检查锁定 - 链接不再有效。 - El Mac
对不起,我是指另一个。 - El Mac
1
@ElMac:Skeet的网站目前无法访问,它将会在适当的时候恢复。我会记住这件事,并确保链接在网站恢复后仍然有效,谢谢。 - Jon
3
自从.NET 4.0版本以来,Lazy<T> 完美地胜任了这个工作。 - ilyabreev
那篇Jon Skeet的文章很有启发性,但在使用双重检查锁定的解决方案中对于volatile的使用却令人困惑。他在示例中省略了它,然后提供了模糊/相互矛盾的评论。我猜测它应该被使用(如果你首先要使用那种单例模式的话,但你可能不应该这样做),但我不确定。 - MarredCheese
我认为锁操作是否真的那么昂贵,非常依赖于上下文,这是一个高度有争议的问题。如果您的设计使线程不会不断争夺锁,则它很可能不会成为瓶颈。除此之外,即使是在10年后,这仍然是一个很好的答案。 - AsPas

52

懒惰的Lazy<T>版本:

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

    public static Singleton Instance
        => lazy.Value;

    private Singleton() { }
}

需要 .NET 4 和 C# 6.0 (VS2015) 或更新版本。


我在 .Net 4.6.1/C# 6 上使用此代码时遇到了 "System.MissingMemberException: 'The lazily-initialized type does not have a public, parameterless constructor.'" 的错误。 - ttugates
@ttugates,你是对的,谢谢。代码已更新,使用值工厂回调来实现延迟对象。 - andasa
1
不确定这个代码是如何保证线程安全的,因为两个并行的线程可能会同时访问单例实例。您能否详细说明一下? - WiredEntrepreneur
2
@WiredEntrepreneur 我一直在想同样的问题,但似乎Lazy对象本身是线程安全的:https://dev59.com/oWUp5IYBdhLWcg3whn1z#15222942 - Berthim

17

进行锁定操作:相当于一个null测试(仍然比null测试昂贵)。

在另一个线程占用锁时进行锁定操作:您需要承担在锁定时其他线程仍需要完成的成本,加上您自己的时间成本。

在另一个线程占用锁时进行锁定操作,并且有数十个其他线程也在等待该锁:非常低效。

出于性能方面的考虑,您总是希望尽可能短的持有其他线程想要获取的锁。

当然,与狭窄锁相比,我们更容易理解“宽泛”锁,因此值得从宽泛锁开始并根据需要进行优化,但有些情况需要根据经验和熟悉度选择更狭窄的锁。

(顺便说一下,如果可能只使用 private static volatile Singleton instance = new Singleton() 或者如果可能不使用单例而使用静态类,这两种方式都更好地解决了这些问题)。


1
我真的很喜欢你在这里的想法。这是一个很好的看待它的方式。我希望我能接受两个答案或者+5这个,非常感谢。 - Wayne Phipps
2
当我们需要关注性能时,一个重要的后果是共享结构之间的差异,这些结构可能会同时被访问,也可能不会。有时候我们并不期望这种行为经常发生,但它确实可能发生,因此我们需要锁定(只需要一次锁定失败就会毁掉一切)。其他时候,我们知道很多线程确实会同时访问相同的对象。还有其他时候,我们并没有预料到会有很多并发,但我们错了。当你需要提高性能时,具有大量并发的任务优先考虑。 - Jon Hanna
在你的情况下,volatile 不是必需的,但应该使用 readonly。请参阅 https://dev59.com/hGct5IYBdhLWcg3wWMFF。 - wezten

9
原因是性能。如果instance != null(除了第一次之外,这总是成立的),就不需要进行昂贵的lock操作:同时访问已初始化的单例的两个线程将不必要地同步。

5

杰弗里·里希特建议如下:



    public sealed class Singleton
    {
        private static readonly Object s_lock = new Object();
        private static Singleton instance = null;
    
        private Singleton()
        {
        }
    
        public static Singleton Instance
        {
            get
            {
                if(instance != null) return instance;
                Monitor.Enter(s_lock);
                Singleton temp = new Singleton();
                Interlocked.Exchange(ref instance, temp);
                Monitor.Exit(s_lock);
                return instance;
            }
        }
    }


将实例变量声明为volatile,不是也能起到同样的作用吗? - Ε Г И І И О

4
在几乎所有情况下(即:除了最开始的那些情况),instance 不会为空。获取锁比简单检查更费成本,因此在锁定之前检查一次 instance 的值是一个好的、免费的优化。
这种模式称为双重检查锁定:http://en.wikipedia.org/wiki/Double-checked_locking

4

这被称为双重检查锁定机制,首先,我们将检查实例是否已创建。如果没有,则仅同步方法并创建实例。它将极大地提高应用程序的性能。执行锁定是很重的。因此,为了避免锁定,我们首先需要检查null值。这也是线程安全的,并且是实现最佳性能的最佳方式。请参阅以下代码。

public sealed class Singleton
{
    private static readonly object Instancelock = new object();
    private Singleton()
    {
    }
    private static Singleton instance = null;

    public static Singleton GetInstance
    {
        get
        {
            if (instance == null)
            {
                lock (Instancelock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
}

1

根据您的应用程序需求,您可以热切地创建一个线程安全的单例实例,这是简洁的代码,尽管我更喜欢@andasa的懒惰版本。

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

    private Singleton() { }

    public static Singleton Instance()
    {
        return instance;
    }
}

0
另一个版本的Singleton,在应用程序启动时,下面的代码行创建Singleton实例。
private static readonly Singleton singleInstance = new Singleton();

在这里,CLR(公共语言运行时)将负责对象初始化和线程安全。 这意味着我们不需要显式编写任何代码来处理多线程环境中的线程安全。

“单例设计模式中的急切加载不是一个过程, 在该过程中,我们需要在应用程序启动时初始化单例对象,而不是按需初始化,并将其准备在内存中,以备将来使用。”

public sealed class Singleton
    {
        private static int counter = 0;
        private Singleton()
        {
            counter++;
            Console.WriteLine("Counter Value " + counter.ToString());
        }
        private static readonly Singleton singleInstance = new Singleton(); 

        public static Singleton GetInstance
        {
            get
            {
                return singleInstance;
            }
        }
        public void PrintDetails(string message)
        {
            Console.WriteLine(message);
        }
    }

从主函数:

static void Main(string[] args)
        {
            Parallel.Invoke(
                () => PrintTeacherDetails(),
                () => PrintStudentdetails()
                );
            Console.ReadLine();
        }
        private static void PrintTeacherDetails()
        {
            Singleton fromTeacher = Singleton.GetInstance;
            fromTeacher.PrintDetails("From Teacher");
        }
        private static void PrintStudentdetails()
        {
            Singleton fromStudent = Singleton.GetInstance;
            fromStudent.PrintDetails("From Student");
        }

不错的替代方案,但并没有回答问题,问题是关于在问题中提到的特定实现中的锁定检查。 - Wayne Phipps
“线程安全的C#单例模式” - Jaydeep Shil

0

反射攻击安全的单例模式:

public sealed class Singleton
{
    public static Singleton Instance => _lazy.Value;
    private static Lazy<Singleton, Func<int>> _lazy { get; }

    static Singleton()
    {
        var i = 0;
        _lazy = new Lazy<Singleton, Func<int>>(() =>
        {
            i++;
            return new Singleton();
        }, () => i);
    }

    private Singleton()
    {
        if (_lazy.Metadata() == 0 || _lazy.IsValueCreated)
            throw new Exception("Singleton creation exception");
    }

    public void Run()
    {
        Console.WriteLine("Singleton called");
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接