剑指offer之面试题2:实现Singleton模式
来源:剑指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 /// <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
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
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
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的实例创建时机过早的问题:
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#的静态构造函数的特性,确保只创建一个实例。
第五种方法利用了私有嵌套类型的特性,做到只在需要的时候才会创建实例,提高空间使用率。