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

2012-2-19 09:03| 发布者: benben| 查看: 283| 评论: 0

摘要: 引子经常从Recruiter那里得到抱怨:汤姆,为什么面试者每次回去的时候都感觉良好,而你却说此人达不到Senior级别?我都是微笑着说:感觉不一定都是对的哦。Recruiter:那你就不能问点别的么?为什么每次面试者都说你 ...

引子

经常从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

转载于:https://www.cnblogs.com/ppcompany/articles/2710843.html

大叔手记(10):别再让面试官问你单例相关推荐

  1. 当面试官问我ArrayList和LinkedList哪个更占空间时,我这么答让他眼前一亮

    前言 今天介绍一下Java的两个集合类,ArrayList和LinkedList,这两个集合的知识点几乎可以说面试必问的. 对于这两个集合类,相信大家都不陌生,ArrayList可以说是日常开发中用的 ...

  2. 面试官问你B树和B 树,就把这篇文章丢给他

    原文链接:面试官问你B树和B 树,就把这篇文章丢给他 在看这篇文章之前,我们回顾一下前面的几篇关于MySQL的文章,应该对你读下面的文章有所帮助. InnoDB与MyISAM等存储引擎对比 面试官问你 ...

  3. eureka自我保护时间_阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默...

    文章首发:阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默 什么是服务注册? 首先我们来了解下,服务注册.服务发现和服务注册中心的之间的关系. 举个形象的例子,三者之间的关系就好像是供货 ...

  4. 面试官问:JS的继承

    原文作者若川,掘金链接:https://juejin.im/post/5c433e216fb9a049c15f841b 写于2019年2月20日,现在发到公众号声明原创,之前被<前端大全> ...

  5. 面试官问我:Redis 内存满了怎么办

    转载自 想不到!面试官问我:Redis 内存满了怎么办 Redis占用内存大小 Redis的内存淘汰 LRU算法 LRU在Redis中的实现 LFU算法 问题 Redis占用内存大小 我们知道Redi ...

  6. 你以为面试官问的是分布式缓存,其实他想问……

    最近一个哥们去面试某当红大厂了,其中几个他印象深刻的面试题你们品品: 1.介绍下如何对MySQL SQL语句进行分析和优化? 2.Redis 怎样实现的分布式锁? 3.如何实现本地缓存和分布式缓存? ...

  7. 面试官问你是true还是false你可以最后反问他这个

    我们常常看到一些用==号判断是true还是false的面试题,今天就列出来几个,看看到底是true还是false,原因是什么. String s1 = "abc"; String ...

  8. js var是什么类型_面试官问你JS基本类型时他想知道什么?

    点击上方"IT平头哥联盟",选择"置顶或者星标" 一起进步- 面试的时候我们经常会被问答js的数据类型.大部分情况我们会这样回答包括: 1.基本类型(值类型或者 ...

  9. 面试官问你期待工资多少时,该怎么回答?

    面试时,面试官问我期望工资是多少,我想都没想就要了6000元月薪,然后顺利入职了,可直到2年后,老板给我加薪时,我才知道当初面试的工资要少了,老板教会了我,如果面试官问期望工资是多少,该如何回答,但是 ...

最新文章

  1. 从Promise来看JavaScript中的Event Loop、Tasks和Microtasks
  2. java datetime int_关于jodatime:Java中DateTime对象之间的小数天数
  3. 并发 线程交替执行_并发与并行的区别
  4. Oracle12C的卸载过程
  5. Qt信号与槽传递QList动态数组
  6. 论文笔记_S2D.47_2017-ICRA_SemanticFusion(语义融合):采用卷积神经网络CNN的稠密3D语义建图
  7. 基于钉钉小程序做一个记事本
  8. java 原型模式的应用_java中原型模式详解和使用方法
  9. 【PIC单片机】-- LCD的相关知识
  10. sql面试题:问题1:查询每个同学的学生编号、学生姓名、选课总数...问题2:查询“张三”老师所授课程的学生中,成绩最高的学生信息...
  11. python database is locked_sqlite3.OperationalError: database is locked
  12. 专线多个ip 虚拟服务器,一个云服务器可以做几个ip虚拟机
  13. 获奖感想和Java学习总结
  14. 动态规划之最大非空子段和
  15. 群晖NAS的公网、NAT、DDNS、证书等配置二
  16. 7-104 三天打鱼两天晒网
  17. 前端的Vue相关的项目经验
  18. Matlab2019 slrt(XPC)目标机U盘启动
  19. 面向对象(四)多态以及多态性,
  20. 4. Java并发编程-管程

热门文章

  1. 设计模式-创建型模式-建造者模式
  2. ROS的工作模式和ESXI网卡工作模式的关系
  3. 【系统架构师修炼之道】(13):操作系统基础知识——进程基础知识
  4. request.getParameter和request.getAttribute之间的区别
  5. android listview显示数据库内容
  6. ACM计算几何题目推荐
  7. golang单向散列函数
  8. MySQL笔记4:desc命令的两个用法
  9. 经典PID控制算法用C语言实现!
  10. linux下线程错误码表