剑指Offer面试题:1.实现Singleton模式
说来惭愧,自己在毕业之前就该好好看看《剑指Offer》这本书的,但是各种原因就是没看,也因此错过了很多机会,后悔莫及。但是后悔是没用的,现在趁还有余力,把这本书好好看一遍,并通过C#通通实现一遍,并记录在我的博客中,作为学习笔记。
一、题目:实现Singleton模式
题目:设计一个类,我们只能生成该类的一个实例。
只能生成一个实例的类是实现了Singleton(单例)模式的类型。由于设计模式在面向对象程序设计中起着举足轻重的作用,在面试过程中很多公司都喜欢问一些与设计模式相关的问题。在常用的模式中,Singleton是唯一一个能够用短短几十行代码完整实现的模式。因此,写一个Singleton的类型是一个很常见的面试题。
例如,在一个Flappy Bird游戏中,小鸟这个游戏对象在整个游戏中应该只存在一个实例,所有对于这个小鸟的操作(向上飞、向下掉等)都应该只会针对唯一的一个实例进行。
二、几种不好的解法
2.1 不好的解法一:只适用于单线程环境
public sealed class Singleton1 { private Singleton1() { } private static Singleton1 instance = null; public static Singleton1 Instance { get { if(instance == null) { instance = new Singleton1(); } return instance; } } }
解法一的代码在单线程的时候工作正常,但在多线程的情况下多个线程都会创建一个自己的实例,无法保证单例模式的要求。
2.2 不好的解法二:虽然在多线程环境中能工作但效率不高
public sealed class Singleton2 { private Singleton2() { } private static readonly object syncObject = new object(); private static Singleton2 instance = null; public static Singleton2 Instance { get { // 每个线程来之前先等待锁 lock(syncObject) { if (instance == null) { instance = new Singleton2(); } } return instance; } } }
解法二就保证了我们在多线程环境中也只能得到一个实例,但是加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。
2.3 可行的解法三:加同步锁前后两次判断实例是否已存在
前面讲到的线程安全的实现方式的问题是要进行同步操作,那么我们是否可以降低通过操作的次数呢?其实我们只需在同步操作之前,添加判断该实例是否为null就可以降低通过操作的次数了,这样是经典的Double-Checked Locking方法,修改上面的属性代码如下:
public static Singleton3 Instance { get { // Double-Check 双重判断避免不必要的加锁 if (instance == null) { // 确定实例为空时再等待加锁 lock (syncObject) { // 确定加锁后实例仍然未创建 if (instance == null) { instance = new Singleton3(); } } } return instance; } }
解法三用加锁机制来确保在多线程环境下只创建一个实例,并且用两个if判断来提高效率。但是,这样的代码实现起来比较复杂,容易出错。
三、两种较好的解法
3.1 较好的解法一:利用静态构造函数
C#的语法中有一个函数能够确保只调用一次,那就是静态构造函数。由于C#是在调用静态构造函数时初始化静态变量,.NET运行时(CLR)能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。
public sealed class Singleton4 { private Singleton4() { } // 在大多数情况下,静态初始化是在.NET中实现Singleton的首选方法。 static Singleton4() { } private static readonly Singleton4 instance = new Singleton4(); public static Singleton4 Instance { get { return instance; } } }
该解法是在 .NET 中实现 Singleton 的首选方法,但是,由于在C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时发现第一次使用该类型的时候自动调用该类型的静态构造函数(也就是说在用到Singleton4时就会被创建,而不是用到Singleton4.Instance时),这样会过早地创建实例,从而降低内存的使用效率。此外,静态构造函数由 .NET Framework 负责执行初始化,我们对对实例化机制的控制权也相对较少。
3.2 较好的解法二:实现按需创建实例
public sealed class Singleton5 { private Singleton5() { } public static Singleton5 Instance { get { return Nested.instance; } } // 使用内部类+静态构造函数实现延迟初始化 class Nested { static Nested() { } internal static readonly Singleton5 instance = new Singleton5(); } }
该解法在内部定义了一个私有类型Nested。当第一次用到这个嵌套类型的时候,会调用静态构造函数创建Singleton5的实例instance。如果我们不调用属性Singleton5.Instance,那么就不会触发.NET运行时(CLR)调用Nested,也就不会创建实例,因此也就保证了按需创建实例(或延迟初始化)。
四、总结
在前面的5种实现单例模式的方法中:
第一种方法在多线程环境中不能正常工作,第二种模式虽然能在多线程环境中正常工作但时间效率很低,都不是面试官期待的解法。在第三种方法中我们通过两次判断一次加锁确保在多线程环境能高效率地工作。
第四种方法利用C#的静态构造函数的特性,确保只创建一个实例。第五种方法利用私有嵌套类型的特性,做到只在真正需要的时候才会创建实例,提高空间使用效率。