单例模式(Singleton)

背景

在Unity开发中,单例是非常实用并且是最简单的设计模式。
顾名思义,单例模式就是:

保证类仅有一个实例对象,并且提供唯一的全局访问点。

简单来讲,单例模式必须具备以下条件:

  1. 单例类只有一个实例对象。
  2. 单例类必须给其他所有对象提供这一实例。
  3. 单例类构造函数只能在内部调用。

再引用一个比较有意思的例子,这个例子非常形象的介绍了单例模式:

俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公 Singleton,她们只要说到“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)。-《泡妞与设计模式》

单例模式实现

单例模式分为懒汉式和饿汉式。

  • 懒汉式:当程序第一次访问单例模式实例时才进行创建。
  • 饿汉式:在单例模式类被加载的时候,单例模式实例就已经被创建。

懒汉式单例实现

懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton
{
private static Singleton mInstance;

//私有构造函数,避免在外界创建实例
private Singleton() { }

public static Singleton Instance
{
get
{
//当程序第一次访问单件模式实例时才进行创建。
if (mInstance == null)
mInstance = new Singleton();
return mInstance;
}
}
}

饿汉式单例实现

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton
{
//在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
private static Singleton mInstance = new Singleton();

//私有构造函数,避免在外界创建实例
private Singleton() { }

public static Singleton Instance
{
get
{
return mInstance;
}
}
}

线程安全的单例模式

线程安全的懒汉式单例

从线程安全性上讲,不加同步的懒汉式是线程不安全的,两个线程可能同时判断if (mInstance == null)结果为true,于是都创建了实例,这样就违背了单例模式。那么如何实现线程安全的懒汉式的单例呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Singleton
{
private static Singleton mInstance;

//定义一个保证线程同步的标识
private static readonly object mLocker = new object();

//私有构造函数,避免在外界创建实例
private Singleton() { }

public static Singleton Instance
{
get
{
lock (mLocker)
{
if (mInstance == null)
mInstance = new Singleton();
return mInstance;
}
}
}
}

这个实现是线程安全的。

  1. 当第一个线程运行到这里时,此时会对mLocker对象 “加锁”。
  2. 当第二个线程运行该方法时,首先检测到mLocker对象为”加锁”状态,该线程就会挂起等待第一个线程解锁。
  3. mLocker语句运行完之后(即线程运行完之后)会对该对象”解锁”。

双重检查加锁

由于锁机制的代价比较高,每次都要判断锁机制,比较消耗性能,那么有没有更好的方式实现呢?
可以使用”双重检查加锁”的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。那么什么是”双重检查加锁”机制呢?
所谓双重检查加锁机制指的是:并不是每次获取Instance都需要同步,进入方法体后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Singleton
{
private static Singleton mInstance;

//定义一个保证线程同步的标识
private static readonly object mLocker = new object();

//私有构造函数,避免在外界创建实例
private Singleton() { }

public static Singleton Instance
{
get
{
if (mInstance == null)
{
lock (mLocker)
{
if (mInstance == null)
mInstance = new Singleton();
}
}
return mInstance;
}
}
}

泛型单例模式

如果有多个单例类的话,每个类中都有这段代码,那该怎么办呢?
答案是引入泛型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Singleton<T> where T : Singleton<T>, new()
{
private static Singleton<T> mInstance;

private static readonly object mLocker = new object();

private Singleton() { }

public static Singleton<T> Instance
{
get
{
if (mInstance == null)
{
lock (mLocker)
{
if (mInstance == null)
mInstance = new T();
}
}
return mInstance;
}
}
}

可以看到泛型T的基类类型需是Singleton<T>,并且带有一个public的构造函数来实例化对象,但是如果这样,这个单例类岂不是可以在任意的地方实例化对象了吗?既然这样,就不能通过约束T:new()来实例化,那该怎么办呢?我们还可以通过反射机制来实例化一个对象,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    
public class Singleton<T> where T : Singleton<T>, new()
{
private static Singleton<T> mInstance;

private static readonly object mLocker = new object();

private Singleton() { }

public static Singleton<T> Instance
{
get
{
if (mInstance == null)
{
lock (mLocker)
{
if (mInstance == null)
{
//获取私有构造函数
var ctors = typeof(T).GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

//获取无参构造函数
var ctor = System.Array.Find(ctors, c => c.GetParameters().Length == 0);
if (ctor == null)
{
throw new System.Exception("Non-Public Constructor() not found! in " + typeof(T));
}

// 通过构造函数,创建实例
mInstance = ctor.Invoke(null) as T;
}
}
}
return mInstance;
}
}
}