C# 之单例模式

一、介绍:

  • 单例模式是软件工程中最著名的模式之一。从本质上来讲,单例是一个允许创建自身的单个实例的类,并且通常可以简单的访问该实例。
  • 单例不允许在创建实例时指定任何参数。
  • 单例通常要求他们是懒惰的创建,即直到第一次需要时才创建实例。

单例的实现,有四个共同特征:

  • 单个构造函数,它是私有且无参数的。这可以防止其他类实例化它。
  • 类是密封的。严格来说,由于上述原因,这个不是必要的,但是可以帮助JIT进行更多的优化。
  • 一个静态变量,用于保存对单个已创建实例的引用。(如果有的话)
  • 公共静态意味着获取对单个已创建实例的引用,必要时创建一个实例。

  请注意:所有这些实现还使用公共静态属性Instance 作为访问实例的方法。在所有情况下,可以轻松将属性转换为方法,而不会影响线程安全和性能。

二、单例的6个常见版本:

  • 第一个版本:不是线程安全的:(糟糕的代码,请勿使用)
  public sealed class Singleton1
    {
        private static Singleton1 instance = null;

        private Singleton1() { }

        public static Singleton1 Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new Singleton1();
                }

                return instance;
            }
        }
    }

上面代码,不是线程安全的,两个不同线程都去竞争 if(instance == null) 时候,然后发现为true,会同时创建两个实例,这违反了单例原则。为了防止这种情况,我们想到了互斥锁,保证两个线程不会共同创建实例,请看第二个版本。

  • 第二个版本:简单的线程安全
  public sealed class Singleton2
    {
        private static Singleton2 instance = null;
        private static readonly object padlock = new object();
        Singleton2() { }

        public static Singleton2 Instance
        {
            get
            {
                lock (padlock)//加锁,保证两个线程并发时,不会重复创建对象,但问题是每次线程运行还得检查锁(性能略差)
                {
                    if (instance == null)
                    {
                        instance = new Singleton2();
                    }

                    return instance;
                }
            }
        }
    }

 上述实现时线程安全的,加锁之后,防止多线程并发创建重复对象,但它有性能缺陷,因为每次使用Instance 调用实例时,都需要去判断锁,这样导致性能有影响,于是我们想到了第三个版本,双重判断加锁。

  • 第三个版本,使用双重检查锁定来尝试线程安全(糟糕的代码,请勿使用)
  public sealed class Singleton3
    {
        private static Singleton3 instance = null;
        private static readonly object padlock = new object();

        Singleton3() { }

        public static Singleton3 Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (padlock)
                    {
                        if (instance == null)
                        {
                            instance = new Singleton3();
                        }
                    }
                }

                return instance;
            }
        }
    }

 该实现是线程安全的,不必每次都取出锁,即当第一次创建完实例后,下一次在调用实例,则不必再取出锁了。但该模式有四个缺点

  1. 它在Java中不起作用。
  2. 在没有任何内存障碍的情况下,ECMA CLI规范也打破了这一限制。
  3. 这很容易出错。该模式需要完全如上所述,任何重大变化都可能影响性能或正确性。
  4. 它的性能不如后续实现。
  •  第四个版本,不太懒,不使用锁且线程安全
  public sealed class Singleton4
    {
        private static readonly Singleton4 instance = new Singleton4();

        static Singleton4() { }//显示静态构造函数告诉C# 编译器
        private Singleton4() { }

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

为什么说他不太懒惰?因为所谓懒惰是指我们要在调用实例时才去判断是否创建实例instance,然而这里,在类里先创建了实例。

为什么说他是线程安全的?因为 C# 中静态构造函数仅在 创建类的实例 或 引用类的静态成员时执行,举个例子,例如我在调用Instance 这个实例时,会执行第一句  instance = new Singleton4(),当我第二次再调用Instance 使用静态实例时,它则不会重复创建实例,所以它是线程安全的。

  • 第五个版本,完全懒惰实例化
    public sealed class Singleton5
    {
        private Singleton5() { }

        public static Singleton5 Instance { get { return Nested.instance; } }//完全懒惰的,因为必须在执行这个Instance调用时,才会执行嵌套类的创建单例语句

        private class Nested
        {
            static Nested() { }

            internal static readonly Singleton5 instance = new Singleton5();//此处要为internal的,外部要访问,不能是私有的。此处在外部调用时,需要创建单例对象时创建,实现完全懒惰
        }

    }

 上述代码是懒惰的,因为在封闭类中调用子类的instance时才去创建实例,而不是像第四个版本那样先创建了实例。请注意,尽管嵌套类可以访问封闭类中的私有成员,但是封闭类不能访问嵌套类内层,所以这里的嵌套类实例需要使用internal 关键字声明。

  • 第六个版本:使用.NET 4 的Lazy 类型。
    public sealed class Singleton6
    {
        private static readonly Lazy<Singleton6> lazy = new Lazy<Singleton6>(() => new Singleton6());

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

        private Singleton6() { }
    }

 这里使用.NET4 或者更高版本,可以使用System.Lazy 这个类型声明懒惰的,线程安全的单例,同时他的性能非常好。

三、懒惰与性能

在许多情况下,其实不需要完全懒惰,除非您的初始化做了一些特别耗时的事情,或者其他地方产生了一些副作用,否则最好忽略上面所示的显示静态构造函数。这可以提高性能,因为它允许JIT编译器进行一次检查(例如在方法的开头)以确保类型已经初始化,然后从那时开始设定它。如果在相对紧密的循环中引用单例实例,则会产生(相对)显著的性能差异。您应该决定是否需要完全延迟实例化,并在类中适当地记录此决策。

参考原文:https://www.cnblogs.com/leolion/p/10241822.html

原文地址:https://www.cnblogs.com/vpersie2008/p/12272696.html