1. 什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
种类:懒汉式和饿汉式
2. 单例模式的设计原理
单例模式的核心目标是确保一个类只有一个实例。为实现这一目标,我们需要考虑以下几个关键设计原理:
2.1 私有构造函数(Private Constructor)
通过将构造函数设为私有,我们防止外部直接实例化类,从而强制要求通过指定的方法获取类的实例。
2.2 静态实例(Static Instance)
通过维护一个静态实例,并提供一个静态方法来获取该实例,我们确保在整个应用程序生命周期内只有一个类的实例存在。
2.3 线程安全性(Thread Safety)
在多线程环境下,需要考虑单例模式的线程安全性。通过使用互斥锁等机制,我们可以确保在多线程情况下仍然只有一个实例被创建。
3. 单例模式的实现方式
单例模式有多种实现方式,每种方式都有其优缺点。以下是两种常见的实现方式:
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
3.1 懒汉式(Lazy Initialization)
1. 私有构造函数
确保类的跟赋值有关的构造函数以及析构函数是私有的,这样外部无法直接实例化对象。
class T{
private:
T(){};//默认构造
T(const T &)=delete;//拷贝构造
T& operator=(const T &)=delete;//赋值构造
T(const T &&)=delete;//移动构造
T& operator=(const T &&)=delete;//移动赋值构造
~T(){};//析构函数
};
- 为什么要把赋值有关的构造设为私有?
- 全局唯一性: 单例模式的目标之一是确保在整个应用程序中只有一个类的实例。通过将构造函数设为私有,防止了外部代码直接实例化对象,从而保障了单例的唯一性。
- 控制实例化过程: 将构造函数设为私有允许单例类控制对象的实例化过程。单例类可以在必要时进行懒加载(Lazy Initialization)、初始化一些资源、或执行其他必要的步骤。
- 全局访问点: 单例模式通常提供一个全局访问点(如静态方法)来获取单例实例。私有构造函数确保外部代码无法直接通过
new
操作符创建对象,只能通过提供的静态方法获取单例。
- 为什么要把析构设为私有?
- 避免手动删除实例: 如果析构函数是私有的,外部代码无法直接调用
delete
来手动销毁单例实例。这可以防止在程序的其他地方意外删除了单例对象。 - 确保唯一性: 确保单例对象只能通过特定的方式进行销毁,例如在程序结束时自动调用析构函数。
- 资源管理: 如果单例类管理一些资源(如打开的文件、网络连接等),在析构函数中释放这些资源是一个合理的做法。
2.静态实例
在类内部维护一个静态成员变量,用于保存类的唯一实例。这个静态成员变量需要在类外进行定义和初始化,并在类加载时就创建实例。
class T{
private:
static std::mutex _mutex;//懒汉式需要加锁
static T* instance;//类的唯一实例
private:
T(){};
T(const T &)=delete;
T& operator=(const T &)=delete;
T(const T &&)=delete;
T& operator=(const T &&)=delete;
~T(){};
};
T* T::instance = nullptr;//类的外部赋值为空
3. 静态方法获取实例以及静态释放示例
提供一个公共的静态方法,用于获取类的实例。在这个方法内部,注册释放函数,并且直接返回创建好的实例(用到Double-Check Locking)。
提供一个私有的静态方法,用于释放类的实例。
class T{
public:
//静态获取示例
static T* GetInstance(){
//用到了Double-Check Locking(两次检查一次加锁)
if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
std::lock_guard<std::mutex> lock(_mutex);
if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
instance = new T;
atexit(Destructor);//退出时候调用内存释放函数
}
}
return instance;
}
//静态释放示例
private:
static void Destructor(){
if(instance!=nullptr)
delete instance;
}
private:
static std::mutex _mutex;
static T* instance;
private:
T(){};
T(const T &)=delete;
T& operator=(const T &)=delete;
T(const T &&)=delete;
T& operator=(const T &&)=delete;
~T(){};
};
T* T::instance = nullptr;
-
如果不使用Double-Check Locking会怎么样?(如下)
static T* GetInstance(){ if(instance==nullptr){ instance = new T; atexit(Destructor);//退出时候调用内存释放函数 } return instance; }
试想一下,如果两个线程同时判断instance为空,那么它们都会去实例化一个T对象,这就变成双例了。所以,我们要解决的是线程安全问题。
如何解决呢?
最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子
static T* GetInstance(){
std::lock_guard<std::mutex> lock(_mutex);
if(instance==nullptr){
instance = new T;
atexit(Destructor);//退出时候调用内存释放函数
}
return instance;
}
这样就规避了两个线程同时创建instance对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。
因此用 Double-Check Locking可以很好的规避这个问题
static T* GetInstance(){
//用到了Double-Check Locking(两次检查一次加锁)
if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
std::lock_guard<std::mutex> lock(_mutex);
if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
instance = new T;
atexit(Destructor);//退出时候调用内存释放函数
}
}
return instance;
}
4.完整测试代码
class T{
public:
static T* GetInstance(){
//用到了Double-Check Locking(两次检查一次加锁)
if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
std::lock_guard<std::mutex> lock(_mutex);
if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
instance = new T;
atexit(Destructor);//退出时候调用内存释放函数
}
}
return instance;
}
private:
static void Destructor(){
if(instance!=nullptr)
delete instance;
}
private:
//static std::unique_ptr<T> instance;
static std::mutex _mutex;
static T* instance;
T(){};
T(const T &)=delete;
T& operator=(const T &)=delete;
T(const T &&)=delete;
T& operator=(const T &&)=delete;
~T(){};
};
T* T::instance = nullptr;
int main(){
T* obj1 = T::GetInstance();
T* obj2 = T::GetInstance();
if(obj1 == obj2 )
{
std::cout<< "这两个是相同的实例" <<std::endl;
};
}
懒汉式的优缺点:
优点:
- 延迟加载: 懒汉式在需要时才创建实例,避免了在程序启动时就创建实例,从而节省了资源。这对于某些资源开销较大的对象是比较合适的。
缺点:
- 线程不安全: 懒汉式在多线程环境中可能存在线程安全问题。当多个线程同时检测到实例为
nullptr
,然后同时创建实例时,就会导致多个实例的产生。 - 性能影响: 由于需要在每次获取实例时进行同步(加锁),可能会对性能产生一定的影响。
3.2 饿汉式(Eager Initialization)
代码示例(和懒汉式大致相同)
class T{
public:
static T* GetInstance(){
// std::lock_guard<std::mutex> lock(_mutex); 不需要锁
if(instance==nullptr)
{
instance = new T;
atexit(Destructor);
}
return instance;
}
private:
static void Destructor(){
if(instance!=nullptr)
delete instance;
}
private:
//static std::mutex _mutex; 不需要加锁
static T* instance;
private:
T(){};
T(const T &)=delete;
T& operator=(const T &)=delete;
T(const T &&)=delete;
T& operator=(const T &&)=delete;
~T(){};
};
T* T::instance = new T();//直接实例化 在类之前
int main(){
T* obj1 = T::GetInstance();
T* obj2 = T::GetInstance();
if(obj1 == obj2 )
{
std::cout<< "这两个是相同的实例" <<std::endl;
};
}
饿汉式的优缺点:
优点:
- 线程安全: 饿汉式天生就是线程安全的,因为在类加载时就已经创建好实例,不会存在多个线程同时创建实例的情况。
- 简单: 实现简单,不需要考虑线程同步的问题。
缺点:
- 资源浪费: 在程序启动时就创建实例,如果该实例占用的资源较大,可能会导致资源浪费。
- 不灵活: 无法做到延迟加载,如果在程序运行过程中实例未被使用,也会占用一定的内存。
4.单例的拓展
/ 定义单例模式模板类
template <typename T>
class Singleton {
public:
// 获取单例实例的静态方法
static T& GetInstance() {
static T instance;
return instance;
}
protected:
// 虚析构函数,确保基类可以在需要时正确清理资源
virtual ~Singleton() {}
// 隐藏构造函数和赋值操作符,确保只能通过 GetInstance 获取实例
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(const Singleton&&) = delete;
};
// 定义一个名为 Design 的类,继承自 Singleton 模板类
class Design : public Singleton<Design> {
// 允许 Singleton 模板类访问 Design 的私有成员
friend class Singleton<Design>;
private:
// 私有构造函数,确保只能通过 Singleton::GetInstance 获取实例
Design() {}
// 私有析构函数,确保只能在需要时销毁实例
~Design() {}
public:
// 一个示例方法,用于显示一条信息
void Display() {
std::cout << "Design Singleton Instance" << std::endl;
}
};
int main() {
// 获取 Design 单例的实例
Design& designInstance = Design::GetInstance();
// 使用单例实例的方法
designInstance.Display();
return 0;
}
5. 单例模式的应用场景
单例模式在许多场景中都能够发挥关键作用,确保系统中某个类只有一个实例。以下是一些常见的应用场景:
5.1 资源管理器(Resource Managers)
例如数据库连接池、线程池等资源的管理,确保全局只有一个资源管理器实例。
5.2 配置管理器(Configuration Managers)
在系统中存储配置信息,并通过单例模式确保全局只有一个配置管理器。
5.3 日志记录器(Logging)
在应用程序中记录日志的组件,通过单例模式确保所有的日志信息被统一记录。
5.4 对话框框架(Dialog Framework)
在图形用户界面(GUI)应用程序中,确保只有一个对话框框架管理所有对话框的显示。
6. 总结
(1)单例模式常见的写法有两种:懒汉式、饿汉式
(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
例模式确保所有的日志信息被统一记录。