来源:剑指offer
这篇主要记录《剑指offer》书籍中的面试题2:实现Singleton模式
使用语言:C#
代码环境:VS2017
总共有5中解法,从前往后依次优化。
结构如下:
前言
这里先给出调用程序的代码
Program.cs
代码语言:javascript复制 class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
//Task.Run(() =>
//{
// Console.WriteLine(Singleton1.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton1.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton2.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton2.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton3.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton3.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton4.Instance);
//});
//Task.Run(() =>
//{
// Console.WriteLine(Singleton4.Instance);
//});
Task.Run(() =>
{
Console.WriteLine(Singleton5.Instance);
});
Task.Run(() =>
{
Console.WriteLine(Singleton5.Instance);
});
Console.ReadKey();
}
}
这里,会在每次创建一种Singleton模式的实现方法之后,在这里调用。
里面会用两个线程来模拟多线程的情况。
而在单例的实现中,会在创建构造函数时,输出语句,来区别是否创建了多个对象。
效果如下示例:
构造函数只调用了一次。
方法一
单线程情况下的一般实现。
代码如下:
Singleton1.cs
代码语言:javascript复制 1 public sealed class Singleton1
2 {
3 //私有的构造函数
4 private Singleton1()
5 {
6 //Console.WriteLine($"Singleton1生成了...{Guid.NewGuid()}");
7 }
8 private static Singleton1 instance = null;
9 /// <summary>
10 /// 在静态属性Instance中,只有在instance为null时,才创建一个实例以避免重复
11 /// </summary>
12 public static Singleton1 Instance
13 {
14 get
15 {
16 //如果instance为空,则创建实例
17 if (instance == null)
18 {
19 //Thread.Sleep(3000); //模拟多线程同时到达这里
20 instance = new Singleton1();
21 }
22 return instance;
23 }
24 }
25 }
在上述代码中,Singleton1的静态属性Instance中,只有在instance为null的时候才创建一个实例以避免重复创建。
同时我们把构造函数定义为私有函数,这个就可以确保只创建一个实例。
但是这种方法只适合单线程,多线程情况下,就有问题了。
方法二
为了保证多线程环境下,我们还是只能得到一个类型的实例,需要加上一个同步锁。
代码如下:
Singleton2.cs
代码语言:javascript复制 1 public sealed class Singleton2
2 {
3 /// <summary>
4 /// 私有的构造函数
5 /// </summary>
6 private Singleton2()
7 {
8 Console.WriteLine($"Singleton2生成了...{Guid.NewGuid()}");
9 }
10 //用作同步锁
11 private static readonly object syncObj = new object();
12
13 private static Singleton2 instance = null;
14
15
16 public static Singleton2 Instance
17 {
18 get
19 {
20 //添加同步锁
21 lock (syncObj)
22 {
23 //如果instance为null,则新建
24 if (instance == null)
25 {
26 Thread.Sleep(3000);
27 instance = new Singleton2();
28 }
29
30 }
31 return instance;
32 }
33 }
34 }
我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,
当第一个线程加上锁时,第二个线程只能等待。当第一个线程创建出实例之后,第二个线程就不会重复创建实例了,
这样就保证了我们在多线程环境中也只能得到一个实例。
但是呢,这种方法也不完美。我们每次通过属性Instance得到Singleton2的实例,都会试图加上一个同步锁,
而加锁时一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
方法三
我们可以这样:加同步锁前后两次判断实例是否已经存在。
我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。
改进代码如下:
Singleton3.cs
代码语言:javascript复制 1 public sealed class Singleton3
2 {
3 //私有构造函数
4 private Singleton3()
5 {
6 Console.WriteLine($"Singleton3生成了...{Guid.NewGuid()}");
7 }
8 //创建同步锁对象
9 private static readonly object syncObj = new object();
10
11 private static Singleton3 instance = null;
12
13 public static Singleton3 Instance
14 {
15 get
16 {
17 //instance为null,这里可能有两个线程同时到达,即都判断为null的情况
18 if (instance == null)
19 {
20 Thread.Sleep(3000); //会同时有两个线程等在这里
21 //加上同步锁,即只放一个线程过去
22 lock (syncObj)
23 {
24 //如果此时instance还未null,则新建instance;否则,跳过
25 if(instance==null)
26 instance = new Singleton3();
27 }
28 }
29 return instance;
30 }
31 }
32 }
这样的代码实现起来比较复杂,容易出错,我们还有更优秀的解法。
方法四
C# 语法中有一个函数能够保证只调用一次,那就是静态构造函数。
代码如下:
Singleton4.cs
代码语言:javascript复制 1 public sealed class Singleton4
2 {
3 static Singleton4() //静态构造函数
4 {
5 Console.WriteLine($"Singleton4生成了...{Guid.NewGuid()}");
6 }
7
8 private static Singleton4 instance = new Singleton4();
9
10 public static Singleton4 Instance
11 {
12 get
13 {
14 Thread.Sleep(3000);
15 return instance;
16 }
17 }
18 }
由于C# 是在调用静态构造函数时初始化静态变量,.NET运行时能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。
C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时,发现第一次使用一个类型的时候自动调用该类型的静态构造函数。
方法五
这种方法实现的Singleton,可以很好的解决 方法四 中Singleton4的实例创建时机过早的问题:
代码语言:javascript复制 1 public sealed class Singleton5
2 {
3 Singleton5()
4 {
5 Console.WriteLine($"Singleton5生成了...{Guid.NewGuid()}");
6 }
7
8 public static Singleton5 Instance
9 {
10 get
11 {
12 return Nested.instance;
13 }
14 }
15
16 class Nested
17 {
18 static Nested()
19 {
20
21 }
22 internal static readonly Singleton5 instance = new Singleton5();
23 }
24 }
在这段代码中,我们在内部定义了一个私有类型的 Nested。
当我们第一次用到这个嵌套类型的时候,会调用静态构造函数创建Singleton5的实例 instance。
类型Nested只在属性Singleton5.Instance中被用到,由于其私有属性,他人无法使用Nested类型。
因此,当我们第一次试图通过属性Singleton5.Instance得到Singleton5的实例时,会自动调用Nested的静态构造函数创建实例 instance。
总结
推荐解法,方法四,或者方法五
其中方法四利用了C#的静态构造函数的特性,确保只创建一个实例。
第五种方法利用了私有嵌套类型的特性,做到只在需要的时候才会创建实例,提高空间使用率。