单例模式是一种用来创建独一无二的对象的设计模式,以满足相应的应用场景,如系统日志、对话框、设备驱动等对象,这些类仅能有一个实例。单例模式的主要实现方式有两种,分别为饿汉式和懒汉式,两种方法都是将类的构造函数设为私有,禁止在外部构造,并提供一个全局的访问方法让程序的其它模块共享。本文主要对两种单例模式进行简单介绍,并提醒读者双重检查锁机制存在的一些问题及解决方法。
饿汉式(Eager Initialized)
饿汉式即提前初始化,该方法在程序运行之初就对所需资源进行初始化(例如,在进入main函数之前初始化)。为了记录类的唯一实例,很容易想到的就是使用静态成员变量,因此,在类的内部声明一个静态的Singleton指针,并将其构造函数声明为私有的即可。代码如下:
// .h
class Singleton
{
public:
~Singleton();
static Singleton* getInstance() { return m_uniqueInstance; }
private:
Singleton() { std::cout << "Singleton created before running" << std::endl; }
static Singleton* m_uniqueInstance;
// 防止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
// .cpp
Singleton* Singleton::m_uniqueInstance = new Singleton();
Singleton::~Singleton()
{
std::cout << "Singleton deleted." << std::endl;
}
保存该类实例化的静态指针m_uniqueInstance在cpp中类的外部初始化,这样在程序运行之前该类的实例就已经生成好了。由于构造函数被声明为私有,因此该类无法在外部被构造,这样在整个程序运行期间该类就只拥有m_uniqueInstance指向的这一个实例。在C++11中还可使用delete禁止拷贝和赋值操作。
懒汉式(Lazily Initialized)
即延迟初始化,与提前初始化方式不同,该方法仅在第一次调用 getInstance() 方法时才实例化这个对象,代码如下:
// .h
class LazySingleton
{
public:
~LazySingleton();
static LazySingleton* getInstance();
private:
LazySingleton() { std::cout << "LazySingleton created before running" << std::endl; }
static LazySingleton* m_uniqueInstance;
// 防止拷贝和赋值
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static std::mutex m_mutex;
};
// .cpp
LazySingleton* LazySingleton::m_uniqueInstance = nullptr;
std::mutex LazySingleton::m_mutex;
LazySingleton::~LazySingleton()
{
std::cout << "LazySingleton deleted." << std::endl;
}
LazySingleton* LazySingleton::getInstance()
{
if (m_uniqueInstance == nullptr) // 双重检查锁
{
m_mutex.lock();
if (m_uniqueInstance == nullptr)
{
m_uniqueInstance = new LazySingleton();
}
m_mutex.unlock();
}
return m_uniqueInstance;
}
为了确保在多线程环境下正确调用 getInstance() 方法,这里使用了双重检查锁(DCLP)机制,即第一次判断指针时不加锁,因为只有在指针为空时才需要获取锁,由于此时可能有其它线程都通过了这一判断,因此判断完成后需要再检查一遍,并且让当前线程获取锁,以确保只有当前线程能进行实例化。
双重检查锁问题
这么做看似只有第一个调用 getInstance() 方法的线程能实例化一个单例,保证了线程安全,但双重检查锁机制本身也是存在问题的,主要问题就出在 m_uniqueInstance = new LazySingleton();
上述代码在双重检查后会new一个类的实例,而new操作做了以下三件事:
-
为 LazySingleton 对象分配一片内存;
-
构造一个 LazySingleton 对象,存入已分配的内存区;
-
将 m_uniqueInstance 指向这片内存区;
正常思路的new操作就是上述所描述的顺序那样,然而编译器有可能并不是按照上述步骤生成代码,他可能将步骤1和步骤3写成一条语句,然后才执行步骤2,这就导致上述代码变成:
LazySingleton* LazySingleton::getInstance() {
if (m_uniqueInstance == nullptr) {
m_mutex.lock();
if (m_uniqueInstance == nullptr) {
m_uniqueInstance = // Step 3
operator new(sizeof(LazySingleton)); // Step 1
new (m_uniqueInstance) LazySingleton; // Step 2
}
}
return m_uniqueInstance;
}
这样的代码在多线程下就会有问题:
1. 线程A进入getInstance(),检查出m_uniqueInstance为空,请求加锁,而后执行由步骤1和步骤3组成的语句。之后线程A被挂起。此时,m_uniqueInstance已为非空指针,但m_uniqueInstance指向的内存里的LazySingleton对象还未被构造出来。
2. 线程B进入getInstance(), 检查出m_uniqueInstance非空,直接将m_uniqueInstance返回(return)给调用者,这样线程B就会得到一个未初始化过的LazySingleton对象,这显然是错误的。
解决方法
1.使用延迟初始化时,尽量在每个需要使用singleton对象的线程开始时,只调用一次instance(),之后该线程就可直接使用缓存在局部变量中的指针。
Singleton* const instance = Singleton::getInstance(); // cache instance pointer
instance->func1();
instance->func2();
instance->func3();
而不是像这样
Singleton::getInstance()->func1();
Singleton::getInstance()->func2();
Singleton::getInstance()->func3();
2.使用提前初始化(eager initialization)方式,即在程序运行之初就对所需资源进行初始化(例如,在进入main函数之前初始化)。
3.使用 std::once_flag 和 std::call_once 进行延迟初始化,std::call_once 可以保证多个线程对函数只调用一次。