什么是单件模式
一个类只有一个实例,并为之提供一个全局的访问点。
如何实现
把类的构造函数私有化,禁用类的拷贝构造函数、赋值函数和移动构造函数。
并且提供一个全局访问静态成员函数,使外部可以直接获取或初始化实例。
class Singleton
{
public:
~Singleton(){}
static Singleton* getInstance()
{
if (nullptr == m_pInstance)
{
m_pInstance = new Singleton();
}
return m_pInstance;
}
static void delMe()
{
if (nullptr != m_pInstance)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
private:
Singleton(){}
Singleton(Singleton& other) = delete;
Singleton(Singleton&& other) = delete;
Singleton& operator=(Singleton& other) = delete;
private:
static Singleton* m_pInstance;
};
Singleton* Singleton::m_pInstance = nullptr;
以上代码是一个单件模式的简单实现,当然这个实现在多线程下并不安全。
单件模式的类型
我们根据实例构建的时机将单件模式分为两类:
饿汉模式
在类加载时已经创建好实例。
将上面代码的最后一行改成以下代码即可。
优点:实现简单,因为实例是在main执行之前初始化的,所以调用时不需要初始化实例,线程安全。
缺点:同样因为实例是在main执行前初始化,所以影响main的执行速度,而且会提前占用内存。
无法控制实例的构造顺序以及依赖关系。
// 直接初始化静态成员
Singleton* Singleton::m_pInstance = new Singleton();
懒汉模式
在第一次需要调用的时候,才创建实例。
以上简单实现的代码即为懒汉模式。
优点:在首次需要调用的时候初始化,不会造成内存浪费。可以精准控制初始化的顺序和依赖关系。
缺点:非线程安全,多线程调用需要加锁,影响性能。
线程安全
template <class T>
class Singleton
{
private:
const Singleton& operator=(const Singleton&);
Singleton(const Singleton&);
protected:
Singleton()
{
cout << "Singleton constructor" << endl;
}
~Singleton()
{
cout << "Singleton desconstructor" << endl;
}
static T* m_pInstance;
/// 多线程用
static mutex m_mutex;
public:
static T* getMe()
{
if (nullptr == m_pInstance)
{
m_mutex.lock();
if (nullptr == m_pInstance)
{
m_pInstance = new T();
}
m_mutex.unlock();
}
return m_pInstance;
}
static void delMe()
{
if (nullptr != m_pInstance)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
template <class T>
T* Singleton<T>::m_pInstance = nullptr;
template <class T>
mutex Singleton<T>::m_mutex;
以上为线程安全的懒汉模式。
也可以通过pthread_once函数来实现线程安全代码,原理和上面代码差不多。
补充:
static T* getMe()
{
if (nullptr == m_pInstance)
{
m_mutex.lock();
if (nullptr == m_pInstance)
{
m_pInstance = new T();
}
m_mutex.unlock();
}
return m_pInstance;
}
关于这段代码中,在读程序员的自我修养(P29)时发现存在问题。问题来源于CPU的乱序执行,代码中的m_pInstance = new T(); 包含三个步骤:
- 分配内存。
- 在内存位置上调用构造函数。
- 将内存地址赋给m_pInstance。
其中2、3的顺序是可以颠倒的。所以可能出现一种情况:m_pInstance的值已经不为nullptr,但对象的构造函数还没有完成,这显然不是我们想要看到的情况。
这里想要保证线程安全,必须阻止CPU换序,可以通过barrier指令来实现。代码如下:
#define barrier() __asm__ volatile ("lwsync")
static T* getMe()
{
if (nullptr == m_pInstance)
{
m_mutex.lock();
if (nullptr == m_pInstance)
{
T* temp = new T();
barrier(); // 保证线程安全
m_pInstance = temp;
}
m_mutex.unlock();
}
return m_pInstance;
}
写在最后
如果对内存要求没那么高,可以在多线程未创建时,用懒汉模式在主线程初始化单件实例,以避免使用线程锁。