【C++】单例模式(饿汉模式、懒汉模式)
C++单例模式总结与剖析
饿汉单例模式 C++实现
C++单例模式(饿汉式)
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结
,一共有23种经典设计模式
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性
设计模式使代码编写真正工程化,设计模式是软件工程的基石脉络,如同大厦的结构一样
单例模式是设计模式中最常用的一种模式,一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享
基础要点:
- 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
- 线程安全
- 禁止赋值和拷贝
- 用户通过接口获取实例:使用 static 类成员函数
单例的实现主要有饿汉式和懒汉式两种,分别进行介绍
饿汉式不管你将来用不用,程序启动时就创建一个唯一的实例对象
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定
示例代码:
// Hunger_Singleton_pattern // Created by lei on 2022/05/13 #include#include using namespace std; class Example { public: typedef shared_ptr Ptr; static Ptr GetSingleton() { cout << "Get Singleton" << endl; return single; } void test() { cout << "Instance location:" << this << endl; } ~Example() { cout << "Deconstructor called" << endl; }; private: static Ptr single; Example() { cout << "Constructor called" << endl; }; Example &operator=(const Example &examp) = delete; Example(const Example &examp) = delete; }; Example::Ptr Example::single = shared_ptr (new Example); int main() { Example::Ptr a = Example::GetSingleton(); Example::Ptr b = Example::GetSingleton(); a->test(); b->test(); cout << "main end" << endl; return 0; }
打印输出:
Constructor called Get Singleton Get Singleton Instance location:0x55d43c9dce70 Instance location:0x55d43c9dce70 main end Deconstructor called
可以看到拷贝构造函数只调用了一次,并且两个对象内存地址相同,说明该类只能实例化一个对象
饿汉单例模式的静态变量的初始化由C++完成,规避了线程安全问题,所以饿汉单例模式是线程安全的
在大多数情况下使用饿汉单例模式是没有问题的
有缺陷的懒汉模式懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存
// Defect_Lazy_Singleton_pattern // Created by lei on 2022/05/13 #include#include using namespace std; class Singleton { private: Singleton() { cout << "constructor called!" << endl; } Singleton(const Singleton &) = delete; Singleton& operator=(const Singleton &) = delete; static Singleton *m_instance_ptr; public: ~Singleton() { cout << "destructor called!" << endl; } static Singleton *get_instance() { if (m_instance_ptr == nullptr) { m_instance_ptr = new Singleton; } return m_instance_ptr; } void use() const { cout << "in use" << endl; } }; Singleton *Singleton::m_instance_ptr = nullptr; //静态成员变量类内声明类外初始化 int main() { Singleton *instance = Singleton::get_instance(); Singleton *instance_2 = Singleton::get_instance(); // thread t1(Singleton::get_instance); // thread t2(Singleton::get_instance); // thread t3(Singleton::get_instance); // thread t4(Singleton::get_instance); // t1.join(); // t2.join(); // t3.join(); // t4.join(); return 0; }
打印输出:
constructor called!
取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,存在以下问题
1、当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance_ptr
是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr
还是空的,于是也开始实例化单例;这样就会实例化出两个对象
2、类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用,因此会导致内存泄漏
改进的懒汉模式对应上面两个问题,有以下解决方法:
1、用mutex加锁
2、使用智能指针
// Improve_Lazy_Singleton_pattern // Created by lei on 2022/05/13 #include#include // shared_ptr #include // mutex #include using namespace std; class Singleton { public: typedef shared_ptr Ptr; ~Singleton() { cout << "destructor called!" << endl; } Singleton(const Singleton &) = delete; Singleton& operator=(const Singleton &) = delete; static Ptr get_instance() { // "double checked lock" if (m_instance_ptr == nullptr) { lock_guard lk(m_mutex); if (m_instance_ptr == nullptr) { m_instance_ptr = shared_ptr (new Singleton); } } return m_instance_ptr; } private: Singleton() { cout << "constructor called!" << endl; } static Ptr m_instance_ptr; static mutex m_mutex; }; // initialization static variables out of class Singleton::Ptr Singleton::m_instance_ptr = nullptr; mutex Singleton::m_mutex; int main() { Singleton::Ptr instance = Singleton::get_instance(); Singleton::Ptr instance2 = Singleton::get_instance(); // thread t1(Singleton::get_instance); // thread t2(Singleton::get_instance); // thread t3(Singleton::get_instance); // thread t4(Singleton::get_instance); // t1.join(); // t2.join(); // t3.join(); // t4.join(); return 0; }
打印输出:
constructor called! destructor called!
只构造了一次实例,并且发生了析构
缺陷是双检锁依然会失效,具体原因可以看下面的文章
推荐的懒汉模式https://www.drdobbs.com/cpp/c-and-the-perils-of-double-checked-locki/184405726
// Recommand_Lazy_Singleton_pattern // Created by lei on 2022/05/13 #include#include using namespace std; class Singleton { public: ~Singleton() { cout << "destructor called!" << endl; } Singleton(const Singleton &) = delete; Singleton &operator=(const Singleton &) = delete; static Singleton &get_instance() { static Singleton instance; return instance; } private: Singleton() { cout << "constructor called!" << endl; } }; int main() { Singleton &instance_1 = Singleton::get_instance(); Singleton &instance_2 = Singleton::get_instance(); // thread t1(Singleton::get_instance); // thread t2(Singleton::get_instance); // thread t3(Singleton::get_instance); // thread t4(Singleton::get_instance); // t1.join(); // t2.join(); // t3.join(); // t4.join(); return 0; }
打印输出:
constructor called! destructor called!
这种方法又叫做 Meyers’ Singleton Meyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全
- 不需要使用共享指针,代码简洁
- 注意在使用的时候需要声明单例的引用 Single& 才能获取对象
C++11于once flag,call_once:分析的实现
C++11实现线程安全的单例模式(使用std::call_once)
在多线程编程中,有一个常见的情景是某个任务仅仅须要运行一次
在C++11中提供了非常方便的辅助类once_flag与call_once
once_flag和call_once的声明:
struct once_flag
{
constexpr once_flag() noexcept;
once_flag(const once_flag&) = delete;
once_flag& operator=(const once_flag&) = delete;
};
template
void call_once(once_flag& flag, Callable&& func, Args&&... args);
} // std
简单示例:
// once_flag and call_once simple example // Created by lei on 2022/05/13 #includeusing namespace std; once_flag flag; void do_once() { call_once(flag, [&]() { cout << "Called once" << endl; }); } int main() { std::thread t1(do_once); std::thread t2(do_once); std::thread t3(do_once); std::thread t4(do_once); t1.join(); t2.join(); t3.join(); t4.join(); }
打印输出:
Called once
可以看到4个线程只执行了一次do_once( )函数
call_once实现单例模式// Call_once_Singleton_pattern // Created by lei on 2022/05/13 #include#include #include using namespace std; once_flag cons_flag; class A { public: typedef shared_ptr Ptr; void m_print() { cout << "m_a++ = " << ++m_a << endl; } static Ptr getInstance(int a) { cout << "Get instance" << endl; if (m_instance_ptr == nullptr) { lock_guard m_lock(m_mutex); if (m_instance_ptr == nullptr) { call_once(cons_flag, [&]() { m_instance_ptr.reset(new A(a)); }); } } return m_instance_ptr; } ~A() { cout << "Deconstructor called" << endl; } private: static mutex m_mutex; int m_a; static Ptr m_instance_ptr; A(int a_) : m_a(a_) { cout << "Constructor called" << endl << "m_a = " << m_a << endl; } A &operator=(const A &A_) = delete; A(const A &A_) = delete; }; A::Ptr A::m_instance_ptr = nullptr; mutex A::m_mutex; void test(int aa) { cout << "Go in test..." << endl; A::Ptr tp = A::getInstance(aa); cout << "tp location:" << tp << endl; tp->m_print(); cout << endl; } int main() { thread t1(test, 1); thread t2(test, 2); thread t3(test, 3); thread t4(test, 4); t1.join(); t2.join(); t3.join(); t4.join(); cout << "main end..." << endl; return 0; }
打印输出:
Go in test... Get instance Constructor called m_a = 4 tp location:0x7fd964000f30 m_a++ = 5 Go in test... Get instance tp location:0x7fd964000f30 m_a++ = 6 Go in test... Get instance tp location:0x7fd964000f30 m_a++ = 7 Go in test... Get instance tp location:0x7fd964000f30 m_a++ = 8 main end... Deconstructor called
看到构造函数只调用了一次,并且类A实例化对象的地址始终相同
上面的两个示例程序中都用到了lambda表达式,call_once通常结合lambda一起使用



