【c++设计模式06】创建型4:单例模式(Singleton Pattern)
原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/132326928
qq技术交流群:921273910
类型 | 序号 | 设计模式 | 描述 |
创建型 | 1 | 简单工厂模式 (Simple Factory Pattern) | 通过一个工厂类负责创建所有产品的实例 |
2 | 工厂方法模式 (Factory Pattern) | 将对象的实例化延迟到子类中实现 | |
3 | 抽象工厂模式 (Abstact Factory Pattern) | 通过提供一组相关产品的接口,实现了一系列具体工厂类来创建不同产品族的实例 | |
4 | 单例模式 (Singleton Pattern) | 保证一个类只有一个实例 | |
5 | 创建者模式 (Builder Pattern) | 如何创建一个组合对象 | |
6 | 原型模式 (Prototype Pattern) | 它通过复制已有对象来创建新的实例 |
当我们需要确保一个类只有一个实例时,可以使用单例模式。单例模式是一种创建型设计模式,它限制了类的实例化,使得在整个应用程序中只能存在一个实例。
一、定义
单例模式保证一个类只有一个实例,并且提供一个全局访问点来获取该实例。
二、适用场景
单例模式适用于以下情况:
- 当一个类只能有一个实例,且该实例需要提供给整个系统访问时。
- 当需要控制某个资源的共享访问权限时,例如数据库连接池、线程池等。
(比如我们上一节讲的工厂,工厂就可以采用单例模式)
三、确保,一个类可以实例化一个对象
-
构造函数私有化,在类内部只调用一次,这个是可控的。
- 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为私有的。
- 在类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
-
拷贝构造函数私有化或者禁用(使用 = delete)
-
拷贝赋值操作符重载函数私有化或者禁用(从单例的语义上讲这个函数已经毫无意义,所以在类中不再提供这样一个函数,故将它也一并处理一下。)
-
UML类图
四、分类
单例模式有两种常见的实现方式:懒汉式和饿汉式。
1、懒汉式——首次访问时才创建实例
懒汉式指的是在首次访问时才创建实例。
以下是懒汉式的示例代码:
#include<iostream>
using namespace std;
class Singleton {
private:
static Singleton* instance;
// 私有构造函数,防止从外部实例化对象
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
};
Singleton* Singleton::instance = nullptr;
int main()
{
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
if (s1 == s2) {
cout << "单例模式" << endl;
}
else {
cout << "不是单例模式" << endl;
}
return 0;
}
输出
在调用getInstance()函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()函数,在这个函数内部每个线程都会new出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。
-
优点:
- 延迟加载:只有在首次访问时才会创建实例,节约了系统资源。
- 线程安全:在不加入额外处理的情况下,可以实现基本的线程安全。
-
缺点:
- 不是线程安全的:在多线程环境下,如果多个线程同时调用 getInstance() 方法,可能会导致创建多个实例。(这个发生的概率极低,在线程安全章节我们细讲)
- 性能问题:每次调用 getInstance() 方法都需要进行锁定和解锁操作,会影响性能。
-
线程不安全demo
2、饿汉式——类加载时就创建实例
饿汉式指的是在类加载时就创建实例。
以下是饿汉式的示例代码:
class Singleton {
private:
static Singleton* instance;
// 私有构造函数,防止从外部实例化对象
Singleton() {}
public:
static Singleton* getInstance() {
return instance;
}
};
Singleton* Singleton::instance = new Singleton();
- 优点:
- 线程安全:在类加载时就创建实例,保证了线程安全。
- 缺点:
- 提前加载:即使该实例在整个应用程序的生命周期中没有被使用,也会在类加载时被创建,占用了系统资源。
- 不支持延迟加载:由于实例在类加载时就被创建,因此无法实现懒加载。
五、线程安全性深入讨论(懒汉式单例模式)
1、懒汉式单例真的线程不安全吗?——理论上是
- 结论:理论上是线程不安全的。
2、线程不安全是怎么发生的?——前面线程同时进入初始化
if (instance == nullptr)
{
instance = new Singleton();
}
- 两个或多个线程同时调用了
Singleton::getInstance()
, - 前面几个线程又同时运行到了上面的代码这里。(我们假设是10线程中的前2个线程,后面8个线程都是在初始化
instance = new Singleton()
完成后进入的,这样后面8个线程就不会进入初始化了,用的都是前面线程初始化的instance
) - 细分:在第一个线程未完成初始化时,
instance == nullptr
在两个线程间是同时成立的,所以同时进入。 - 这样
理论上
可能发生:第一个初始化的线程返回的实例和第二个线程返回的实例不是同一个instance
(static是可以重复赋值的,只会保存一个而已)
3、写一个线程不安全的demo复现,证明线程不安全?——做不到
- 在第二步我们已经介绍清楚了,线程不安全发生的情况,是否可以证明呢?
证明不了 - 为何无法证明线程不安全?
条件太苛刻了,要求两个线程同时运行到一处代码,几乎是不可能发生的。 - 是否可以启动n多个线程都调用
Singleton::getInstance()
复现出线程不安全?
可以启动n个线程,但是证明办不到;我们可以启动多个线程同时调用Singleton::getInstance()
,但是我们明白线程不安全仅仅发生在instance未初始化时,一旦初始化完成,后面进入的线程,便是线程安全的。 - 还是写一个证明线程不安全的demo吧,虽然无法证明线程不安全。(其实我写了很多种形式,都无法证明线程不安全,主要是条件太苛刻,只是理论上的线程不安全)
#include<iostream>
#include <thread>
#include<future>
#include<string>
using namespace std;
class Singleton {
private:
static Singleton* instance;
// 私有构造函数,防止从外部实例化对象
Singleton() {}
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
};
Singleton* Singleton::instance = nullptr;
void create() {
Singleton* singleton = Singleton::getInstance();
cout << "Thread " << this_thread::get_id() << ": Singleton address: " << singleton << endl;
}
int main() {
cout << "=================使用thread创建线程========================\n\n";
const int numThreads = 5;
thread threads[numThreads];
for (int i = 0; i < numThreads; ++i) {
threads[i] = thread(create);
}
for (int i = 0; i < numThreads; ++i) {
threads[i].join();
}
cout << "=================使用async创建线程========================\n\n";
future<void> s1 = async(launch::async, create);
future<void> s2 = async(launch::async, create);
s1.wait();
s2.wait();
return 0;
}
输出:可以看到单例模式的地址都是一样的,线程id不同
4、这个理论上不是线程安全的单例模式,如何变成理论上安全的的线程模式?——加锁
这个懒汉式单例模式,既然是理论上是线程不安全的,是否我们就可以不管不顾,把他当作线程安全的来使用呢?——理论上是不可以的,实际上是可以的。但是作为一名严谨的程序员,我们是不允许理论不安全的可能出现的。所以有了下面两种方式。
- 方案一:加锁
所有多线程的安全问题,基本都可以通过加锁来解决。
#include<iostream>
#include <thread>
#include<future>
#include<mutex>
using namespace std;
static std::mutex m_mutex;
class Singleton {
private:
static Singleton* instance;
static std::mutex m_mutex;
// 私有构造函数,防止从外部实例化对象
Singleton() {}
public:
static Singleton* getInstance()
{
m_mutex.lock();
if (instance == nullptr) {
instance = new Singleton();
}
m_mutex.unlock();
return instance;
}
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::m_mutex; // 静态成员变量定义和初始化
std::mutex mut;
void create() {
Singleton* singleton = Singleton::getInstance();
std::lock_guard<std::mutex> guard(mut);
cout << "Thread " << this_thread::get_id() << ": Singleton address: " << singleton << endl;
}
int main() {
cout << "=================使用thread创建线程========================\n\n";
const int numThreads = 5;
thread threads[numThreads];
for (int i = 0; i < numThreads; ++i) {
threads[i] = thread(create);
}
for (int i = 0; i < numThreads; ++i) {
threads[i].join();
}
cout << "=================使用async创建线程========================\n\n";
future<void> s1 = async(launch::async, create);
future<void> s2 = async(launch::async, create);
s1.wait();
s2.wait();
return 0;
}
- 方案二:双重检查锁定
《方案一加锁的问题》每次进入Singleton::getInstance()
都会加锁和释放锁,在后面不需要初始化的时候,也是需要加锁和取消锁,是浪费时间的。
为了解决这个问题,使用双重检查锁定(double-checked locking)。
以下是使用双重检查锁定的示例代码:
static Singleton* getInstance()
{
if (nullptr == instance)
{
m_mutex.lock();
if (instance == nullptr) {
instance = new Singleton();
}
m_mutex.unlock();
}
return instance;
}
这样,在第一个线程初始化完成后,后面就不需要再加锁和解锁浪费时间了。
六、总结
单例模式是一种创建型设计模式,用于确保一个类只有一个实例。它适用于需要控制资源共享访问权限的场景。单例模式可以分为懒汉式和饿汉式两种实现方式,并且需要注意线程安全性,可以使用互斥锁或双重检查锁定来保证线程安全。