目录
[1. 什么是单例模式](#1. 什么是单例模式)
[2. 为什么要进行单例模式](#2. 为什么要进行单例模式)
[3. 如何实现单例模式](#3. 如何实现单例模式)
[4. 单例模式涉及到的问题](#4. 单例模式涉及到的问题)
[5. 单例模式的设计思路](#5. 单例模式的设计思路)
1. 什么是单例模式
单例模式是指在一个系统的全生命周期过程中,只允许一个类实例化一个对象,并提供实例化该对象的一个全局访问点。
2. 为什么要进行单例模式
主要是为了防止一个类实例化多个对象造成数据不一致问题或者造成资源消耗问题。
3. 如何实现单例模式
主要包含以下步骤:
(1)私有化构造函数
(2)禁用拷贝构造和赋值运算符:防止外部通过拷贝构造和赋值直接创造出对象
(3)定义一个静态指针或引用:用于指向类的唯一实例
(4)提供静态公有成员函数:用于返回类的唯一实例的指针或引用
示例:
cpp
#include <iostream>
#include <memory>
class Singleton {
public:
// 静态公有成员函数,返回类的唯一实例
static Singleton& getInstance() {
// 静态局部变量在第一次调用时初始化,且只初始化一次
return only;
}
// 示例成员函数
void doSomething() {
std::cout << "Doing something in Singleton!" << std::endl;
}
private:
// 私有化构造函数,防止外部直接创建实例
Singleton() {};
// 私有化析构函数
~Singleton() {};
// 禁止拷贝构造函数和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
//定义一个静态指针或者引用
//在 Singleton 类内部声明了一个静态私有成员变量 only,用于存储该类的唯一实例。
static Singleton only;
};
//静态成员变量的声明仅告诉编译器该成员的存在,而实际内存分配和初始化需要在类的外部进行定义。
Singleton Singleton::only;
int main() {
// 获取单例实例并调用成员函数
Singleton& singleton = Singleton::getInstance();
singleton.doSomething();
// 尝试再次获取单例实例(应为同一个实例)
Singleton& anotherSingleton = Singleton::getInstance();
anotherSingleton.doSomething();
return 0;
}
4. 单例模式涉及到的问题
线程安全:多条线程在执行对共享数据的代码时,需要保证各线程正常且正确的执行,不会出现数据污染情况。
如何保证线程安全:
(1)给共享资源加锁,保证每个资源变量每时每刻至多被一个线程占用
(2)让线程也拥有资源,不用去共享进程中的资源。如:使用threadlocal可以为每个线程维护一个私有的本地变量。
5. 单例模式的设计思路
(1)饿汉模式:系统一运行,就初始化创建实例,当需要时,直接调用即可。(也就是在类内直接创建静态局部变量)
优点:线程安全,在类加载的时候就创建实例,不存在多线程环境下的线程安全问题(还没进入主函数就创建完实例了,所以不用担心线程安全问题)。
缺点:可能会造成资源浪费:在程序运行过程中始终存在实例,可能会占用一定的资源。不支持延迟加载:无法实现延迟加载的特性。
(2)懒汉模式:指的是类对象在使用时才会被创建。(也就是在getinstance函数中创建静态局部变量)
优点:调用时创建实例,节省资源
缺点:存在线程安全问题
cpp
/// 内部静态变量的懒汉实现 //
//头文件
class Single
{
public:
// 获取单实例对象
static Single& GetInstance();
// 打印实例地址
void Print();
private:
// 禁止外部构造
Single();
// 禁止外部析构
~Single();
// 禁止外部拷贝构造
Single(const Single &single) = delete;
// 禁止外部赋值操作
const Single &operator=(const Single &single) = delete;
};
//源文件
Single& Single::GetInstance()
{
/**
* 局部静态特性的方式实现单实例。
* 静态局部变量只在当前函数内有效,其他函数无法访问。
* 静态局部变量只在第一次被调用的时候初始化,也存储在静态存储区,生命周期从第一次被初始化起至程序结束止。
*/
static Single single;
return single;
}
void Single::Print()
{
std::cout << "我的实例内存地址是:" << this << std::endl;
}
Single::Single()
{
std::cout << "构造函数" << std::endl;
}
Single::~Single()
{
std::cout << "析构函数" << std::endl;
}
以上线程也是安全的:
1.静态局部变量在程序启动阶段就已经被分配内存空间了,但是它的的初始化却是在第一次运行到它的时候,如果我们不调用GetInstance()方法,这个静态局部变量是不会被初始化的。
2.在多线程的情况下,可能会出现对个线程同时访问GetInstance()的情况,但是静态局部变量的初始化,在汇编指令上,已经自动添加了线程互斥指令了,所以总的来说是安全的。
利用互斥锁实现懒汉模式:
cpp
/// 加锁的懒汉式实现 //
//头文件
class SingleInstance
{
public:
// 获取单实例对象
static SingleInstance *GetInstance();
//释放单实例,进程退出时调用
static void deleteInstance();
// 打印实例地址
void Print();
private:
// 将其构造和析构成为私有的, 禁止外部构造和析构
SingleInstance();
~SingleInstance();
// 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
SingleInstance(const SingleInstance &signal);
const SingleInstance &operator=(const SingleInstance &signal);
private:
// 唯一单实例对象指针
static SingleInstance *m_SingleInstance;
static std::mutex m_Mutex;
};
//源文件
//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = nullptr;
std::mutex SingleInstance::m_Mutex;
// 注意:不能返回指针的引用,否则存在外部被修改的风险!
SingleInstance * SingleInstance::GetInstance()
{
// 这里使用了两个 if 判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,
// 避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的。
if (m_SingleInstance == nullptr)
{
std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
if (m_SingleInstance == nullptr)
{
volatile auto temp = new (std::nothrow) SingleInstance();
m_SingleInstance = temp;
}
}
return m_SingleInstance;
}
void SingleInstance::deleteInstance()
{
std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
if (m_SingleInstance)
{
delete m_SingleInstance;
m_SingleInstance = nullptr;
}
}
void SingleInstance::Print()
{
std::cout << "我的实例内存地址是:" << this << std::endl;
}
SingleInstance::SingleInstance()
{
std::cout << "构造函数" << std::endl;
}
SingleInstance::~SingleInstance()
{
std::cout << "析构函数" << std::endl;
}