参考:
C++ 单例模式总结与剖析——这篇文章写得很好。
单例模式
——这篇文章的代码很详细。
1.单例模式简介
单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
要点:
1)全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
2)线程安全
3)禁止赋值和拷贝
4)用户通过接口获取实例:使用 static 类成员函数
2.实现方式
2.1 饿汉式
饿汉式就是静态成员直接初始化,这样就保证了静态变量只被初始化一次,可以确保多个线程使用的是同一个单例对象。
缺点是在不使用单例的时候也需要进行初始化,内存占用较大。
示例代码:
#include <iostream>
class Singleton
{
private:
Singleton() {
std::cout << "constructor called\n";
}
~Singleton() {
std::cout << "destructor called\n";
}
Singleton(Singleton&) = delete;//拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//赋值运算符重载
static Singleton* m_pSingleton;
public:
static Singleton* getInstance() {
return m_pSingleton;
}
};
Singleton* Singleton::m_pSingleton = new Singleton;
int main(int argc, char const *argv[])
{
Singleton* p1 = Singleton::getInstance();
Singleton* p2 = Singleton::getInstance();
std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
return 0;
}
执行结果:
2.2 懒汉式
2.2.1 有缺陷的懒汉模式
懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象。好处是如果被调用就不会占用内存。
#include <iostream>
class Singleton
{
private:
Singleton() {
std::cout << "constructor called\n";
}
~Singleton() {
std::cout << "destructor called\n";
}
Singleton(Singleton&) = delete;//拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//赋值运算符重载
static Singleton* m_Instance;
public:
static Singleton* getInstance() {
if(m_Instance == nullptr)//这里会导致线程不安全
m_Instance = new Singleton;
return m_Instance;
}
static void Delete() {
if(m_Instance)
delete m_Instance;
m_Instance = nullptr;
}
};
Singleton* Singleton::m_Instance = nullptr;
int main(int argc, char const *argv[])
{
Singleton* p1 = Singleton::getInstance();
Singleton* p2 = Singleton::getInstance();
std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
return 0;
}
在单线程时,该程序可以正常运行,但是在多线程时,会出现错误,因为多个线程可能同时判断m_Instance
为nullptr
,然后执行m_Instance = new Singleton;
#include <iostream>
#include <thread>
class Singleton
{
private:
Singleton() {
std::cout << "constructor called\n";
}
~Singleton() {
std::cout << "destructor called\n";
}
Singleton(Singleton&) = delete;//拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//赋值运算符重载
static Singleton* m_Instance;
public:
static Singleton* getInstance() {
if(m_Instance == nullptr)
m_Instance = new Singleton;
return m_Instance;
}
static void Delete() {
if(m_Instance)
delete m_Instance;
m_Instance = nullptr;
}
};
Singleton* Singleton::m_Instance = nullptr;
static Singleton *p1, *p2;
void fun1()
{
p1 = Singleton::getInstance();
}
void fun2()
{
p2 = Singleton::getInstance();
}
int main(int argc, char const *argv[])
{
int times = 0;
while(1){
std::thread* th1 = new std::thread(fun1);
std::thread* th2 = new std::thread(fun2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if(p1 && p2){
if(p1 != p2){
break;//两者不同,说明Singleton::getInstance()中的new Singleton执行了两次
}
else{
Singleton::Delete();//删除原来的
times++;
delete th1;
delete th2;
continue;//继续执行循环,直到在多线程中出现new Singleton执行了两次的情况
}
}
}
std::cout << "try times = " << times << "\n";
std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
return 0;
}
可以看出上述代码在不满足线程安全的要求,在多线程的情况下可能会出现错误。
2.2.2 双重锁加智能指针
双重锁解决并发问题,智能指针解决内存泄漏问题。
#include <iostream>
#include <memory> //shared_ptr
#include <mutex> //mutex
class Singleton
{
private:
Singleton(){ std::cout << "constructor called\n"; }
~Singleton(){ std::cout << "destructor called\n"; }
Singleton(Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
static std::shared_ptr<Singleton*> m_instance_ptr;
static std::mutex m_mutex;
public:
static std::shared_ptr<Singleton*> get_instance(){
//双重锁
if(m_instance_ptr == nullptr){
std::lock_guard<std::mutex> lk(m_mutex);
if(m_instance_ptr == nullptr){
m_instance_ptr = std::shared_ptr<Singleton*>(new Singleton);
}
}
return m_instance_ptr;
}
};
std::shared_ptr<Singleton*> Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::mutex;
int main(){
std::shared_ptr<Singleton*> p1 = Singleton::get_instance();
std::shared_ptr<Singleton*> p2 = Singleton::get_instance();
return 0;
}
shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。
还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!这是由于编译器编译完的执行顺序与我们理解的执行顺序并不一致。具体参考C++多线程——原子操作atomic对内存模型的介绍。
2.2.3 推荐的懒汉式单例——使用局部静态变量
#include <iostream>
class Singleton
{
private:
Singleton() {
std::cout << "constructor called\n";
}
~Singleton() {
std::cout << "destructor called\n";
}
Singleton(Singleton&) = delete;//拷贝构造函数
Singleton& operator=(const Singleton&) = delete;//赋值运算符重载
public:
static Singleton& getInstance() {
static Singleton m_Instance;
return m_Instance;
}
};
int main(int argc, char const *argv[])
{
Singleton& instance_1 = Singleton::getInstance();
Singleton& instance_2 = Singleton::getInstance();
std::cout << "instance_1's address" << &instance_1 << "\t" << "instance_2's address " << &instance_2 << "\n";
return 0;
}
局部静态变量不仅只会初始化一次,而且还是线程安全的。
注意getInstance函数现在返回的是引用。
这种单例被称为Meyers’ Singleton。这种方法很简洁,也很完美,但是注意:
gcc 4.0之后的编译器支持这种写法。
C++11及以后的版本(如C++14)的多线程下,正确。
C++11之前不能这么写。