单例模式的结构非常简单,如下类图所示。在单例模式种,需要注意多线程的问题,多线程会导致不一定遵循单例,本文将在代码实现种进行讨论。首先在该模式种,我们需要将构造函数、拷贝构造函数设置为私有的,如果不这么做C++编译器将会默认给你生产公有的构造函数,这样外界就可以访问到这构造函数了,然后设置静态的变量。
在了解单例模式之前,建议大家先去了解下C++各种锁的知识,有助于理解单例模式中的安全锁的问题。
(1)模式动机
在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?这应该是类设计者的责任,而不是使用者的责任。
(2)模式定义
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
(3)要点总结
a). Singleton模式中的实例构造器一般设置为private,但也可以设置为protected以允许子类派生。
b). Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
c). 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
static Singleton* m_instance;
static mutex m_mutex;
public:
static Singleton* getInstance();
};
Singleton* Singleton::m_instance = nullptr;
// 线程非安全版本
/**
* 当多线程同时进入时,此时m_instance都是nullptr,
* 所以其他线程也获取了这个实例,并不符合单例
*/
Singleton* Singleton::getInstance(){
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
// 线程安全版本,但锁的代价过高
/**
* 因为有锁,所以得等锁释放后其他线程才能进来访问,可以使多线程安全
* 但是 m_instance 已经不是null了,如果是多个线程同时读一个变量,
* 是不需加锁的,写操作才需要加锁。所以此时读操作,太多线程在没必要的等待着
*/
Singleton* Singleton::getInstance(){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
// 双检查锁,但由于内存读写reorder不安全
/**
* 两次判断 m_instance 是否为空,是因为在lock之前,A线程准备执行下一个,
* 但B线程同时进来也在lock等待着,如果不双重检查,就会出现两个线程分别
* 获得两个实例。但是这机制有问题,因为在计算机上有reorder的问题,也就
* 是说在计算机执行的开辟内存、执行构造函数、地址赋值时候顺序可能会被打乱
*/
Singleton* Singleton::getInstance(){
if (m_instance == nullptr) {
Lock lock;
if (m_instance == nullptr) {
m_instance == new Singleton();
}
}
return m_instace;
}
// C++11版本之后的跨平台实(Java、C#采用的是加volatile但不能跨平台)
atomic<Singleton*> Singleton::m_instance;
mutex Singleton::m_mutex;
Singleton* Singleton::getInstance(){
Singleton* tmp = m_instance.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire); //fence是屏障,获取内存的fence
if (tmp == nullptr) {
lock_guard<mutex> lock(m_mutex);
tmp = m_instance.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
atomic_thread_fence(memory_order_release); //释放内存的fence
m_instance.store(tmp, memory_order_relaxed);
}
}
return tmp;
}
比如在进行m_instance = new Singleton()
的时候,我们期望的是这三步:
①先为 m_instance 分配内存
②然后调用 Singleton() 方法构造实例
③最后赋值 m_instance 实例引用
然而编译器优化可能会将 ② ③ 步的顺序调换,这样重排序并不影响单线程的执行结果,JVM是允许的。但是在多线程中就会出问题,这时如果另外一个线程B 拿到了不为null 的instance实例引用,但是并没有被初始化,然后线程B使用了一个没有被初始化的对象引用,就会产生严重的错误,而且这个出现的频率还蛮高。