代码改变世界

大叔手记(10):别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴)

2011-12-19 09:26  汤姆大叔  阅读(31827)  评论(66编辑  收藏

引子

经常从Recruiter那里得到抱怨:“汤姆,为什么面试者每次回去的时候都感觉良好,而你却说此人达不到Senior级别?”

我都是微笑着说:“感觉不一定都是对的哦。”

Recruiter:“那你就不能问点别的么?为什么每次面试者都说你问的是单例?”

我只能解释:“单例挺好的,可以问出很多基础知识哦。”

Recruiter:“大叔,单例我都懂了,不就是程序运行的时候只能有一个实例么?我打电话招人的时候经常都帮你问过了呢!做开发的没几个不懂!”

我Faint。。。

为避免引起误会加注:问这个题目的目的不是仅仅为了单例,而是考察相关的基础知识,比如静态构造函数,私有构造函数,锁,延时创建对象, readonly/const等区别,不会仅以单例论英雄,之所以面试者以为感觉良好,主要是给出其中1-2个单例实现以后就觉得通过了面试,其实没有察觉到面试官所要考察的真正内容。

本文目的

写文本的目的,不是说从单例有多重要,多牛逼啥的。其实更多地是建议博客园的兄弟在面试的时候以另外一个角度来看到面试官的问题,做到主动出击,也就是说当人问你一个问题的时候,绝对不要想着他问的只是问题的表面,可能还隐藏着很多陷阱(因为面试官通常不会有太多时间面试,一般第一次约见都是60-90分钟,所以不太可能问太多问题,只能问几个问题,然后再根据这些问题延时出各种问题),所以在你回答问题的时候,尽量要避开这些陷阱,比如单例里我们经常谈到加锁和线程的问题,如果你对多线程不熟悉,防止陷在里面,那可以赶紧主动说出双锁这种实现方式,然后回头一转话题说:”其实单例要考察我们的东西有很多,比如私有构造函数,静态构造函数,静态字段,readonly和const的区别等等“,其实一般说了这么多以后,面试官基本上不会再在单例上揪住不放了,可能只是象征性问一下构造函数的区别而已,因为这时候他已经知道你基本上了解相关的内容了。当然,如果你想欲擒故纵,就是想让面试官在这个问题上再多问你半小时了,那也可以牵着面试官的鼻子走哦,不过,这个我想一般不太可能吧。

注:这周如果有时间,我会将我去年的一次被面试经历写下来与大家分享的(被印度佬面了将近7个小时,其实就是一道字符串的题目以及延伸,再加一些闲聊)。

下面就列举一下,在面试过程中得到的不同单例版本吧,大家也可以参考一下:

版本1:Recruiter都懂

using System;

public sealed class Singleton
{
private static Singleton instance;

private Singleton() {}

public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}

这个版本的主要问题,就是线程安全的问题,当2个请求同时方式这个类的实例的时候,可以会在同一时间点上都创建一个实例,虽然一般不会出异常错误,但是起码不是我们谈论的只保证一个实例了。

版本2

public sealed class Singleton
{
// 在静态私有字段上声明单例
private static readonly Singleton instance = new Singleton();

// 私有构造函数,确保用户在外部不能实例化新的实例
private Singleton(){}

// 只读属性返回静态字段
public static Singleton Instance
{
get
{
return instance;
}
}
}

标记类为sealed是好的,可以防止被集成,然后在子类实例化,使用在静态私有字段上通过new的形式,来保证在该类第一次被调用的时候创建实例,是不错的方式,但有一点需要注意的是,C#其实并不保证实例创建的时机,因为C#规范只是在IL里标记该静态字段是BeforeFieldInit,也就是说静态字段可能在第一次被使用的时候创建,也可能你没使用了,它也帮你创建了,也就是周期更早,我们不能确定到底是什么创建的实例。

版本3

    public sealed class Singleton
{
// 依然是静态自动hold实例
private static volatile Singleton instance = null;
// Lock对象,线程安全所用
private static object syncRoot = new Object();

private Singleton() { }

public static Singleton Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
instance = new Singleton();
}
}

return instance;
}
}
}

使用volatile来修饰,是个不错的注意,确保instance在被访问之前被赋值实例,一般情况都是用这种方式来实现单例。

版本4

    public class Singleton
{
// 因为下面声明了静态构造函数,所以在第一次访问该类之前,new Singleton()语句不会执行
private static readonly Singleton _instance = new Singleton();

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

private Singleton()
{
}

// 声明静态构造函数就是为了删除IL里的BeforeFieldInit标记
// 以去北欧静态自动在使用之前被初始化
static Singleton()
{
}
}

这种方式,其实是很不错的,因为他确实保证了是个延迟初始化的单例(通过加静态构造函数),但是该静态构造函数里没有东西哦,所以能有时候会引起误解,尤其是在code review或者代码优化的时候,不熟悉的人可能直接帮你删除了这段代码,那就又回到了版本2了哦,所以还是需要注意的,不过如果你在这个时机正好有代码需要执行的话,那也不错。

版本5

    public sealed class Singleton
{
private Singleton()
{
}

public static Singleton Instance { get { return Nested._instance; } }

private class Nested
{
static Nested()
{
}

internal static readonly Singleton _instance = new Singleton();
}
}

这其实是根据版本4的一个变异版本,就不多说了

版本6

    public class Singleton
{
// 因为构造函数是私有的,所以需要使用lambda
private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());
// new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);

private Singleton()
{
}

public static Singleton Instance
{
get
{
return _instance.Value;
}
}
}

其实,一般Lazy的默认构造器只能调用传入泛型类型T的public构造函数的,但是在本例,因为代码是在我们的Singleton内部,所以调用私有的构造函数是没问题的,大家可能还有一个疑虑就是这种方式除了能做到Lazy初始化,做到线程安全么?大家看一下上面一层被注释掉的代码,多了一个LazyThreadSafetyMode.ExecutionAndPublication参数,意思是设置线程安全,但由于Lazy<T>默认的设置就是线程安全,所以不设置也是有效的。

 

所以说,面试的时候,如果能够把上述6个版本的各种实现原理概况说2分钟的话,我估计面试官一般情况不会再在单例上揪住不放了,而且如果你说的基本上都没问题的话,这个时候面试官通常已经开始对你有好感了,起码我是这样的,因为起码你已经将C#的一些基础知识在这个单例问题上体现了不少,不是么?

延伸

当然,如果你这个时候,回答不出那么多的话,我也不会在这个问题上纠缠那么多,免得制造紧张的气氛影响后面的讨论,通常情况下我会及时地切换到另外一个话题上(比如,你简历上写的最强的Skill以便让你重新恢复信心),如果你问答的都不错的话,其实我还想再问一小点,就一小点:如何实现泛型版本的单例?

这个可是加分项哦,之前,有人给了一个如下的代码:

    public class Singleton<T> where T : new()
{
private static readonly Lazy<T> _instance
= new Lazy<T>(() => new T());

public static T Instance
{
get { return _instance.Value; }
}
}

曾经这个版本,我在给自己问这个问题的时候,也考虑过,但其实它是有问题的,就是那个new T(),这样用的话,就默认了T有public的构造函数了,对吧?也就是说,T是有可能存在多个实例的,因为压根就不满足我们的先决条件:那就是你的类不能在外部实例化。

我们来帖一段老外给出的代码:

    public abstract class Singleton
{
private static readonly Lazy<T> _instance
= new Lazy<T>(() =>
{
var ctors = typeof(T).GetConstructors(
BindingFlags.Instance
| BindingFlags.NonPublic
| BindingFlags.Public);
if (ctors.Count() != 1)
throw new InvalidOperationException(String.Format("Type {0} must have exactly one constructor.", typeof(T)));
var ctor = ctors.SingleOrDefault(c => c.GetParameters().Count() == 0 && c.IsPrivate);
if (ctor == null)
throw new InvalidOperationException(String.Format("The constructor for {0} must be private and take no parameters.", typeof(T)));
return (T)ctor.Invoke(null);
});

public static T Instance
{
get { return _instance.Value; }
}
}

从上到下,我们来看看是如何实现的:

  1. 声明抽象类,以便不能直接使用,必须继承该类才能用
  2. 使用Lazy<T>作为_instance,T就是我们要实现单例的继承类
  3. Lazy类的构造函数有一个参数(Func类型),也就是和我们的版本6一样
  4. 根据微软的文档和单例特性,单例类的构造函数必须是私有的,所以这里要加相应的验证
  5. 一旦验证通过,就invoke这个私有的无参构造函数,不用担心他的效率,因为他只执行一次!
  6. Instance属性返回唯一的一个T的实例

我们来实现一个单例类:

    class MySingleton : Singleton<MySingleton>
{
int _counter;

public int Counter
{
get { return _counter; }
}

private MySingleton()
{
_counter = 0;
}

public void IncrementCounter()
{
++_counter;
}
}

这个例子,在一般情况下没问题,但是并行计算的时候还是有问题的,因为上述的泛型版本的代码使用的Lazy<T>能确保我们在创建单例实例的时候是线程安全的,但是不意味着单例本身是线程安全的,我们来做个例子看看:

        static void Main(string[] args)
{
Parallel.For(0, 100, i =>
{
for (int j = 0; j < 1000; ++j)
MySingleton.Instance.IncrementCounter();
});

Console.WriteLine("Counter={0}.", MySingleton.Instance.Counter);
Console.ReadLine();
}

通常Counter的结果是小于100000的,因为单例里的IncrementCounter方法的代码本身不在线程安全的保护之内,所以如果我们想得到准确的100000这个数字的话,我们需要改一下MySingleton的IncrementCounter方法代码:

        public void IncrementCounter()
{
Interlocked.Increment(ref _counter);
}

这样,结果就完美了,至此关于单例的问题就差不多就到这儿了,我们来总结一下单例的优缺点吧。

总结

单例的优点:
1.保证了所有的对象访问的都是同一个实例
2.由于类是由自己类控制实例化的,所以有相应的伸缩性

单例的缺点:
1.额外的系统开销,因为每次使用类的实例的时候,都要检查实例是否存在,可以通过静态实例该解决。
2.无法销毁对象,单例模式的特性决定了只有他自己才能销毁对象实例,但是一般情况下我们都没做这个事情。

同步与推荐

本文已同步至目录索引:《大叔手记全集》

大叔手记:旨在记录日常工作中的各种小技巧与资料(包括但不限于技术),如对你有用,请推荐一把,给大叔写作的动力。

参考文章

Implementing Singleton in C#

The "Double-Checked Locking is Broken" Declaration

Lazy<T> Constructor (Func<T>)

A Generic Singleton Class