动机
- 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能保证它们的逻辑正确性、以及良好的效率。
- 如果绕过常规的构造器,提供一种机制来保证一个类只有一个实例。
- 这应该是类设计者的责任,而不是使用者的责任。
定义
- 保证一个类仅有一个实例,并提供一个该实例的全局访问点。
实现
1 | class Singleton{ |
第一个注意的问题
- 不能让使用者获取到可以直接创建对象的构造函数,如默认构造函数、拷贝构造函数等
- 需要将他们设置为私有或delete
第二个注意的问题
- 上述版本中
getInstance是一个线程不安全的版本,在多线程环境中使用时需要进一步改进
改进一:锁保护
1 | Singleton* Singleton::getInstance() { |
- 这个版本通过锁对
m_instance进行了保护,是一个线程安全的版本 - 但是锁其实仅在第一次创建对象时才会起到真实的保护作用,后续锁就会影响性能
- 根源是因为锁不紧用在了变量写操作,同时作用到了变量读操作,产生了性能消耗
改进二:双检查锁
1 | Singleton* Singleton::getInstance() { |
- 这个版本仅在第一次创建对象时锁才会起作用,后续就不会占用资源,从而避免锁的性能消耗
- 但是这个版本有一个隐藏很深的bug:由于内存读写reorder导致的锁失效问题
- 内存读写reorder
- 多线程是在指令层级抢占cpu时间片的
- 编译器对代码指令级的优化导致实际执行顺序可能发生改变
- 在这个例子中
m_instance = new Singleton();指令可能分为三个步骤:分配内存、调用构造器、返回内存地址 - 实际编译器优化后顺序可能为:分配内存、返回内存地址、调用构造器
- 如果执行到第二个步骤后,另一个线程调用
getInstance()就会拿到一个未执行构造的对象指针,引发bug
改进三:规避reorder问题
- 编译器的支持:关键字
volatile声明变量m_instance()即可 - 跨平台支持(c++11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
总结
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
- Singleton模式一般不需要支持拷贝构造函数和clone接口,因为这可能导致多个对象实例,违背初衷。
- 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。