1、概念
所谓单例模式就是,确保一个类只有一个实例,并提供一个全局访问点。
2、UML图
3、应用场景
举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。
所以,单例模式的应用场景有:
① 需要频繁的进行创建和销毁的对象;
② 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
③ 工具类对象;
④ 频繁访问数据库或文件的对象。
4、实现思路
从单例模式的概念(确保一个类只有一个实例,并提供一个访问它的全局访问点)入手,可以把概念进行拆分为两部分:(1)确保一个类只有一个实例;(2)提供一个访问它的全局访问点。
问:如何确保一个类只有一个实例?
答:定义私有的构造函数就不能在外界通过new创建实例了(通常我们新建的类,在没有写构造函数的情况下,编译器会默认生成一个公有的无参构造函数,因此我们可以通过new来创建类的实例)。
问:那么在哪里创建类的实例呢?
答:在类里边创建。要创建类的实例,就需要一个私有变量来保存该实例,而为了保证在多线程的情况下仅有一个实例,必须为静态变量。
问:外界如何获得该类的实例来使用它呢?
答:定义公有方法提供一个全局访问点,同时也可以定义公有属性来提供全局访问点。
5、实现代码
// 单例模式的实现 public class Singleton { // 定义一个静态变量来保存类的实例 private static Singleton uniqueInstance; // 定义私有构造函数,使外界不能创建该类实例 private Singleton() { } // 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 public static Singleton GetInstance() { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
上面的单例模式的实现在单线程下确实是完美的,然而在多线程的情况下会得到多个Singleton实例,因为在两个线程同时运行GetInstance方法时,此时两个线程判断(uniqueInstance ==null)这个条件时都返回真,此时两个线程就都会创建Singleton的实例,这样就违背了我们单例模式初衷了,既然上面的实现会运行多个线程执行,那我们对于多线程的解决方案自然就是使GetInstance方法在同一时间只运行一个线程运行就好了,也就是需要线程同步。
//单例模式的实现 public class Singleton { // 定义一个静态变量来保存类的实例 private static Singleton uniqueInstance; // 定义一个标识确保线程同步 private static readonly object locker = new object(); // 定义私有构造函数,使外界不能创建该类实例 private Singleton() { } //定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 public static Singleton GetInstance() { // 当第一个线程运行到这里时,此时会对locker对象 "加锁", // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁 // lock语句运行完之后(即线程运行完之后)会对该对象"解锁" // 双重锁定只需要一句判断就可以了 if (uniqueInstance == null) { lock (locker) { // 如果类的实例不存在则创建,否则直接返回 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
疑问:在多线程的GetInstance中为什么要进行两次(uniqueInstance == null)判断呢,是不是只判断一个就好呢?
说明:当uniqueInstance不为null时,只需直接返回uniqueInstance对象即可,就没必要对辅助对象加锁lock,加锁增加了额外开销,损失了性能,因此,我们只需要在lock语句前面加一句(uniqueInstance==null)的判断就可以避免锁所增加的额外开销。这种实现方式叫做 “双重锁定”,这是第一个(uniqueInstance==null)的必要性。在多线程中,如果两个线程同时运行lock外层的if (uniqueInstance == null),都成立,第一条线程加锁实例化一个对象,解锁后,如果不加判断,第二条线程直接实例化一个对象,这就不是单例了。所以第二个(uniqueInstance==null)判断也是必须的。
6、主要优点
① 提供了对唯一实例的受控访问。
② 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
③ 允许可变数目的实例。
7、主要缺点
① 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
② 单例类的职责过重,在一定程度上违背了“单一职责原则”。
③ 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
参考资料: