用心理解设计模式——单例模式 (Singleton Pattern)

前置文章: 用心理解设计模式——设计模式的原则 

设计模式相关代码已统一放至 我的 Github

一、定义

  创建型模式之一。

  Ensure a class has only one instance, and provide a global point of access to it.

(确保某一个类只有一个实例,而且自行实例化并吸纳过整个系统提供这个实例)

二、要点

  将类的非静态构造函数私有化。只在类内部实例一次。

三、评价

  只允许创建单个实例,满足一些管理需求,节省内存,加快对象访问速度等。

四、实现方式

  1.饿汉式

    顾名思义:比较饥渴、比较急,不管用不用,类加载时就先创建实例。

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

    static public Singleton GetInstance()
    {
        return instance;
    }
	
    private Singleton() { }
}

  2.懒汉式

    顾名思义:比较懒,等到用的时候才创建实例。

public sealed class Singleton 
{
    static private Singleton instance;

    static public Singleton GetInstance()
    {
        if (instance == null) { instance = new Singleton(); }
        return instance;
    }
	
    private Singleton() { }
}

两种基本方式比较:

  线程安全?:

    饿汉式,是线程安全的,因为,类只会加载一次,实例必然只会创建一次。

    懒汉式,是线程不安全的,因为,多线程下,如果不同线程同时进入 if判断,就会生成多个实例对象。

  延迟实例化?:

    饿汉式,不管是否使用都会创建实例,如果最终没有使用,造成内存浪费。

    懒汉式,实际使用时才创建实例,节省内存。

3.静态初始化式 (饿汉模式在Lazy Loading上的优化(不彻底)。.Net中首选的方式)

public sealed class Singleton 
{
    static private readonly Singleton instance;

    static public Singleton GetInstance()
    {
        return instance;
    }
    
    //静态构造
    static Singleton()
    {
        instance = new Singleton();
    }
	
    private Singleton() { }
}

  要点理解: C#静态构造函数

  特点:它是线程安全的, 并且将实例化从类加载时延迟到了类首次使用时。

4.双重检查锁模式 (饱汉模式在多线程上的优化)

public sealed class Singleton
{
    private static Singleton instance;

    private static readonly object theLock = new object ();
 
    private Singleton () { }
 
    public static Singleton GetInstance ()
    {
        // 第一重检查, 避免 “实例已经存在时仍然执行lock获取锁, 影响性能” 的问题。
        if (instance == null) 
        {
            //让线程排队执行被lock包围的代码段
            lock (theLock) 
            {
                //第二重检查, 仅让队首的线程进入if创建实例。
                if (instance == null) 
                {
                    instance = new Singleton ();
                }
            }
        }
        return instance;
    }
}

   要点理解: C# 的 lock 关键字  。

   特点:它是线程安全的,并且将实例化延迟到了单例使用时。

5.静态内部类式

public sealed class Singleton
{
    //此静态内部类,只有在第一次使用时才会加载
    static private class Holder
    {
        static public readonly Singleton instance = new Singleton();
    }

    static public Singleton GetInstance()
    {
        return Holder.instance;
    }

    private Singleton() { }
}

  要点理解:内部类在第一次使用时才被加载,其直接在定义时初始化的静态成员也在此时进行初始化。
  特点:它是线程安全的,并且将实例化延迟到了单例使用时。

6.Lazy<T>式

//注意:仅.NET4 以上可用
public sealed class Singleton
{
    static private readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
 
    public static Singleton GetInstance 
    {
        return lazy.Value; 
    }
 
    private Singleton() { }
}

  要点理解:如何执行对象的延迟初始化

  特点:它是线程安全的,并且将实例化延迟到了单例使用时。适合较大单例对象。

五、继承和复用问题

  构造函数被声明为私有的类, 不能被继承。

  因为子类在实例化时需要先调用父类的构造函数,而父类构造函数因为是私有的所以不可访问。

  在C#中尝试,会报错如下图。

  单例类的构造函数被声明为私有,所以单例类不能被继承。 
  通常,单例类会直接声明为sealed,来明确表示不能被继承。

  --------------------------------------------------------------------------------------------------------

  在实际项目中,是有可能出现大量单例类的,如果每个都要实现一遍单例似乎比较麻烦,

  利用泛型可以勉强解决这个问题。 为什么说勉强,请看下面代码。

  ( 这里以 静态初始化式来展示,其他方式类似)。

namespace Singleton.Generic
{
    //泛型单例抽象类
    public abstract class Singleton<T> where T : class, new()
    {
        static private readonly T instance;

        static public T GetInstance()
        {
            return instance;
        }

        //静态构造
        static Singleton()
        {
            // T要能被实例化,必须在where中添加 new() 约束,同时T必须提供一个公有的无参构造函数
            instance = new T();
        }

        //为了能被子类继承,只能放弃父类中对单例的构造函数进行私有化这个要点
        //private Singleton() { }
    }

    //通过继承实现实际的单例类
    public class A : Singleton<A>
    {
        //为了能够被实例化,只能放弃子类中对单例的构造函数进行私有化这个要点
        //private A() { }
    }

    public class Client
    {
        private void Main()
        {
            //期望这样访问
            A instatnce = A.GetInstance();

            //实际因为没私有化构造方法,可以在外部执行多次实例化
            A a = new A();
            A aa = new A();
            A aaa = new A();
        }
    }
}

  这样做的缺点非常明显,

  为了能继承和复用,父子类中都放弃了 “对构造函数进行私有化”这个单例模式要点,实际可以在外部执行多次实例化。

  之后,我又尝试了使用反射 (T)Activator.CreateInstance<T>() 的方式创建实例,这时如果对T的构造函数进行私有化,虽然在编译阶段不会报错,但会在运行时报错:MissingMethodException: Method not found: 'Default constructor not found...ctor() of A'. 可见子类要想在父类中被实例化,必须得提供至少有一个公有的构造函数。(Java中似乎可以通过反射在运行时对构造函数setAccessible,改为public进行构造)

结论:如果想要对单例模式进行继承复用,只能公开构造函数,但是这样违背单例模式的实例限制,不安全,不推荐使用。

六、静态类和单例模式

两者用起来比较相似,让人常常很纠结到底应该选哪个。

从以下几个方面作对比:

1.最直观的,看类是否有成员变量,如果一个类中只是提供一些方法,没有成员变量。那实在没必要使用单例类,直接用静态类即可。

否则,

2.从执行效率,静态类执行效率更高。

3.从成员大小,如果类的职责比较庞大,并且只是在需要的时候才可能用到。就应该考虑是否需要懒加载优化内存,静态类最多推迟到在静态构造方法中初始化静态成员变量,而单例可以推迟到实际使用时。

4.从编程思想,单例类面向对象,可以继承类(注意不能被继承,上边说了),可以有动态多态,可以实现接口。静态类面向过程

5.从使用简便性,静态类直接 类名.方法() ;单例模式要用 类名.GetInstance().方法()。

猜你喜欢

转载自blog.csdn.net/NRatel/article/details/84252744