前置文章: 用心理解设计模式——设计模式的原则
设计模式相关代码已统一放至 我的 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().方法()。