前置文章: 设计模式的原则
其他设计模式:用心理解设计模式专栏
设计模式相关代码已统一放至 我的 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() { }
}
要点理解:如何执行对象的延迟初始化
特点:它是线程安全的,并且将实例化延迟到了单例使用时。适合较大单例对象。
五、通用的单例
在实际项目中,常出现大量单例类,如果每个都要实现一遍单例过程,会比较麻烦。
想要实现通用的单例,要满足两个条件:
1、构造函数设为私有(必须!这是单例的基本要素)。达到单例的目的。
2、提供一个公共的类(公共的父类或工厂类),在这个类中统一构造不同的单例类。达到通用的目的。
但是显然,这两个条件是互相矛盾的! 构造函数为私有的类,不能在其他类中进行构造。
(目前有很多写法都放弃了第1条要素,这对单例来说是极其不安全的。)
那怎么办呢?幸好有反射,“利用反射,可以在使用私有构造函数,在类的外部进行构造”, 这是单例通用的关键。
( 这里以 静态初始化式来展示,其他方式类似)。
using System;
using System.Reflection;
using UnityEngine;
namespace Singleton.Generic
{
//泛型单例抽象类
public abstract class Singleton<T> where T : Singleton<T>
{
static private T instance;
static public T GetInstance()
{
return instance;
}
//静态构造
static Singleton()
{
//instance = new T(); //不可行。需要“公有的无参构造函数”
//instance = (T)Activator.CreateInstance(typeof(T)); //不可行,这种反射构造方式需要“公有的构造函数”
//instance = (T)Assembly.GetAssembly(typeof(T)).CreateInstance(typeof(T).ToString());//不可行。这种反射构造方式需要“公有的构造函数”
//可以利用反射在类外部调用类的私有无参构造函数进行构造!!!
var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);
if (ctor == null)
{
throw new Exception("\"" + typeof(T).ToString() + "\"类中不存在私有无参构造函数");
}
instance = (T)ctor.Invoke(null);
}
//为了能被子类继承,父类的构造必须不能私有
//private Singleton() { }
protected Singleton() { }
}
//通过继承实现单例类
sealed public class A : Singleton<A>
{
private A() { }
}
//通过组合实现单例类(可以继承其他类)
sealed public class B
{
private B() { }
static public B GetInstance()
{
//Singleton<T>类不仅可以当做单例基类,还可作为一个单例工厂(传入类名,传出单例)
return Singleton<B>.GetInstance();
}
}
//直接在客户中,使用单例工厂获取单例
sealed public class C
{
private C() { }
}
public class Client
{
static public void Main()
{
//方式一、继承的单例
A a = A.GetInstance();
//方式二、内部调用静态泛型工厂
B b = B.GetInstance();
//方式三、直接使用静态泛型工厂
C c = Singleton<C>.GetInstance();
//A a2 = new A(); //无法直接构造, 达成单例目的
//B b2 = new B(); //无法直接构造, 达成单例目的
//C c2 = new C(); //无法直接构造, 达成单例目的
//直观说明"继承"和"静态工厂"这两种方式最终都是一个单例。
Debug.Log(A.GetInstance() == Singleton<A>.GetInstance()); //返回true
}
}
}
//Lazy<T> 式
using System;
using System.Reflection;
public abstract class Singleton<T> where T : Singleton<T>
{
static private readonly Lazy<T> lazy = new Lazy<T>(() =>
{
var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);
if (ctor == null) { throw new Exception("\"" + typeof(T).ToString() + "\"类中不存在私有无参构造函数"); }
return ctor.Invoke(null) as T;
});
static public T Instance
{
get { return lazy.Value; }
}
protected Singleton() { }
}
注意: Singleton<T>类在实际使用时,可以让它作为一个单例基类,也可作为一个静态的单例工厂(传入类名,传出单例)。作为单例工厂时,可以解决 “继承自其他类的类,又需要是单例” 的问题。
实际上,因为GetInstance()是一个静态方法(静态方法的继承,本质上只是由子类原封不动地调用父类的该静态方法)。所以Singleton<T>类在作为单例基类时,子类并没用从它身上继承到任何东西,所以,这个基类就是被当做一个静态类在用。两种用法本质上是一样的,都是一个静态单例工厂。
六、静态类和单例模式的比较
这两者,让人常常很纠结到底应该选哪个。
从以下几个方面作对比:
1.最直观的,看类是否有成员变量,如果一个类中只是提供一些方法,没有成员变量。那实在没必要使用单例类,直接用静态类即可。
2.从执行效率,静态类执行效率更高。
3.从成员大小,如果类的职责比较庞大,并且只是在需要的时候才可能用到。就应该考虑是否需要懒加载优化内存,静态类最多推迟到在静态构造方法中初始化静态成员变量,而单例可以推迟到实际使用时。
4.从编程思想,单例类面向对象,可以继承类(注意不能被继承,上边说了),可以有动态多态,可以实现接口。静态类面向过程
5.从使用简便性,静态类直接 类名.方法() ;单例模式要用 类名.GetInstance().方法()。
七、单例常驻内存问题
由于单例模式中总是静态引用一个类的实例,导致该对象始终不会被自动回收,那么,应该根据实际情况,考虑是否主动销毁单例对象。