我们真的会使用单例模式吗?
这篇博客的标题用了一个疑问句,源于我们公司的代码评审,深刻的讨论了单例模式的使用场景及其与静态方法来说有何不同,这次讨论确实让我真正的理解了单例模式的使用,虽然说理解还一定全面,但必须作为一个认知的提升。告诉了我自己,对于编程,不懂的太多,原理性的东西还需要持续的学习。
进入正文,我们来讨论一下,什么是单例模式,何时使用单例模式?
单例模式是经典设计模式的一种,熟悉设计模式或者说读过设计模式相关书籍的同事都知道,这应该算是设计模式中最简单、最容易理解、使用最广泛的一种。单例模式主要是用来实现一个类的实例全局唯一,使用double check的形式来定义。
1 public class SingleInstance 2 { 3 private static readonly object _lock = new object(); 4 private static SingleInstance _instance = null; 5 6 /// <summary> 7 /// 私有构造函数 8 /// </summary> 9 private SingleInstance() { } 10 11 /// <summary> 12 /// 单一实例 13 /// </summary> 14 /// <returns></returns> 15 public static SingleInstance GetInstance() 16 { 17 if (_instance == null) 18 { 19 lock (_lock) 20 { 21 if (_instance == null) 22 { 23 _instance = new SingleInstance(); 24 } 25 } 26 } 27 return _instance; 28 } 29 30 31 public void Show() 32 { 33 Console.WriteLine("输出。。。郭志奇"); 34 } 35 36 public void Speak() 37 { 38 Console.WriteLine("说话。。。郭志奇"); 39 } 40 }
单例模式使用了私有构造函数来保证外部无法实例化、使用double check来保证实例被唯一创建。这是一个基本的单例模式写法,我一般会在其中写一些方法来进行调用,主要是为了避免每次调用都需要new的麻烦。但其中存在一些问题,如果采用静态方法来写:
1 public static void Show() 2 { 3 Console.WriteLine("输出。。。郭志奇"); 4 } 5 6 public static void Speak() 7 { 8 Console.WriteLine("说话。。。郭志奇"); 9 }
比较这两种调用,其实使用方式是一致的,但单例模式会在程序运行中一直存在,不会被销毁,因为单例模式中使用到了静态变量,静态变量的使用会导致实例不会被销毁。但这也不应该是单例模式的缺点。
但我为什么会说我们真的懂单例模式?
回到开头,我们说单例模式,为什么我们需要单例模式,绝对不是因为方便调用,因为静态方法更方便。那到底为什么使用单例模式呢?其实经过我们的讨论,单例模式的使用场景是一些全局不可变参数,可以放到单例中,比如从配置获取值,然后缓存到单例中,这才是我们应当使用单例的场景,千万别像我,为了使用方便而无节制的使用单例。
使用单例,方便调用,但会造成什么问题呢?
要回答这个问题,我们首先回忆一下GC的垃圾回收机制,垃圾回收分为三代,如果类中包含静态成员,垃圾回收机制是不会回收的,也就意味着如果我们无节制的使用单例,会造成程序运行过程中出现大量的实例不会被销毁,会无意识的造成内存使用增高。 如果采用懒加载的方式,在单例未被调用的时候,不会实例化,如果采用饿汉加载的话,那么在程序初始化的时候,就会被初始化,无疑会加重程序的初始化成本,增加启动时间。
如果我们仅仅是为了方便调用,可以使用静态方法。
上面我们说了懒加载方式,我们来代码说明一下饿汉模式的加载方式:
1 public class SingleInstance 2 { 3 private static readonly object _lock = new object(); 4 private static SingleInstance _instance = new SingleInstance(); 5 6 /// <summary> 7 /// 私有构造函数 8 /// </summary> 9 private SingleInstance() { } 10 11 /// <summary> 12 /// 单一实例 13 /// </summary> 14 /// <returns></returns> 15 public static SingleInstance GetInstance() 16 { 17 return _instance; 18 } 19 20 21 public void Show() 22 { 23 Console.WriteLine("输出。。。郭志奇"); 24 } 25 26 public void Speak() 27 { 28 Console.WriteLine("说话。。。郭志奇"); 29 } 30 }
饿汉模式的加载就是静态成员在定义的时候即初始化。
总结:
1、我们应该选择合适的时机使用单例模式,不要无节制的使用,应该明白何时才应该使用单例模式。
2、尽量避免静态成员的使用,因为静态成员所在的实例,不会被GC回收。
3、优先选择静态方法调用而不是单例模式调用。
4、如果必须使用单例模式,尽量采用懒加载,而不是饿汉加载的方式,减少程序启动成本。
引申:
1、我们使用了lock(object)来锁定一个变量,达到加锁的目的,避免多个线程同时对实例执行初始化。那么如果我们lock(string 字符串类型)是否可以呢?答案是否定。
2、System.String和string有什么不同呢?
欢迎有不同见解的同事可以回复讨论,知识总是在讨论中得到升华。