来源:剑指offer
这篇主要记录《剑指offer》书籍中的面试题2:实现Singleton模式
使用语言:C#
代码环境:VS2017
总共有5中解法,从前往后依次优化。
结构如下:
前言
这里先给出调用程序的代码
Program.cs
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
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 ///10 /// 在静态属性Instance中,只有在instance为null时,才创建一个实例以避免重复11 /// 12 public static Singleton1 Instance13 {14 get15 {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
1 public sealed class Singleton2 2 { 3 ///4 /// 私有的构造函数 5 /// 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 Instance17 {18 get19 {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
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 Instance14 {15 get16 {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
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 Instance11 {12 get13 {14 Thread.Sleep(3000);15 return instance;16 }17 }18 }
由于C# 是在调用静态构造函数时初始化静态变量,.NET运行时能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。
C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时,发现第一次使用一个类型的时候自动调用该类型的静态构造函数。
方法五
这种方法实现的Singleton,可以很好的解决 方法四 中Singleton4的实例创建时机过早的问题:
1 public sealed class Singleton5 2 { 3 Singleton5() 4 { 5 Console.WriteLine($"Singleton5生成了...{Guid.NewGuid()}"); 6 } 7 8 public static Singleton5 Instance 9 {10 get11 {12 return Nested.instance;13 }14 }15 16 class Nested17 {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#的静态构造函数的特性,确保只创建一个实例。
第五种方法利用了私有嵌套类型的特性,做到只在需要的时候才会创建实例,提高空间使用率。