一、动机
在软件系统中,经常有这样一种特殊的类,必须保障它们在系统内中存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。如何绕过常规的构建器,提供一个机制来保证一个类只有一个实例。比如Windows
的任务管理器就是很典型的例子,你不能同时打开两个不同的任务管理器。
二、解决方案
单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。单例模式可以:
保证一个类只有一个实例。
为该实例提供一个全局访问节点。
它的运作方式是这样的:如果你创建了一个对象,同时过一会儿后你决定再创建一个新对象,此时你会获得之前已创建的对象,而不是一个新对象。注意,普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。Singleton
模式中的实例构造器可以设置为protected
以允许子类派生。如何实现多线程环境下安全的Singleton
?注意对双检查锁的正确实现。
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有,防止其他对象使用单例类的
new
运算符。- 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。
三、模板模式的结构
单例Singleton
类声明了一个名为getInstance
获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端Client
代码隐藏。 调用获取实例
方法必须是获取单例对象的唯一方式。
四、Singleton
代码示例
1 |
|
线程安全实现之双检查锁
因为存在内存读写乱序(编译器问题),实则不安全。
原因分析:
m_instance = new Singleton()
这句话可以分成三个步骤来执行:
- 分配了一个
Singleton
类型对象所需要的内存;- 在分配的内存处构造
Singleton
类型的对象;- 把分配的内存的地址赋给指针
m_instance
。一般会认为这三个步骤是按顺序执行的,但实际上只能确定步骤
1
是最先执行的,步骤2
和3
却不一定。问题就出现在这。假如某个线程
A
在调用执行m_instance = new Singleton()
的时候是按照1,3,2
的顺序的,那么刚刚执行完步骤3
给Singleton
类型分配了内存(此时m_instance
就不是nullptr
了)就切换到了线程B
,由于m_instance
已经不是nullptr
了,所以线程B
会直接执行return m_instance
得到一个对象,而这个对象并没有真正的被构造!严重bug就这么发生了。
1 |
|
五、使用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建,则返回已有的对象。
- 如果你需要更加严格地控制全局变量,可以使用单例模式。
单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。
请注意,你可以随时调整限制并设定生成单例实例的数量,只需修改
获取实例
方法,即GetInstance
中的代码即可实现。
六、优缺点
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
违反了_单一职责原则_。该模式同时解决了两个问题。单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以你需要想出仔细考虑模拟单例的方法。要么干脆不编写测试代码,或者不使用单例模式。
七、与其它模式的关系
- 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象,那么享元模式就和单例类似了。但这两个模式有两个根本性的不同。
- 只会有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
- 单例对象可以是可变的。享元对象是不可变的。
- 抽象工厂模式、生成器模式和原型模式都可以用单例来实现。