设计模式之单例模式
一、单例模式简介
在单例模式中,类的实例化只会发生一次,而后续的访问都会返回同一个实例。这样可以保证在整个应用程序中,只有一个实例存在,从而避免了多个实例对资源的重复使用或竞争的问题。单例模式通常被用于需要共享某些资源或状态的情况,例如数据库连接、日志记录器、配置管理器等。它可以提供一种简单而有效的方式来管理这些资源,同时确保在整个应用程序中只有一个实例。
单例模式的关键特点包括:
- 私有构造函数:单例类的构造函数被设置为私有,防止外部代码创建多个实例。
- 静态实例变量:单例类内部维护一个静态变量,用于保存类的唯一实例。
- 静态访问方法:通过一个静态的访问方法来获取单例实例,该方法负责创建实例(如果实例不存在)并返回该实例
- 拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。
单例模式可以分为懒汉式 和饿汉式 ,懒汉式和饿汉式是单例模式中两种常见的实现方式,它们在实例化单例对象的时机上有所不同。
-
懒汉式(Lazy Initialization):
- 懒汉式单例模式是在需要时才创建实例。也就是说,当第一次请求获取单例实例时才进行实例化。
- 在懒汉式中,单例对象的实例化是延迟进行的,因此也被称为延迟加载。
- 懒汉式实现相对简单,但在多线程环境下需要考虑线程安全性,需要进行同步控制,以避免多个线程同时创建多个实例。
-
饿汉式(Eager Initialization):
- 饿汉式单例模式在程序启动时就创建实例。也就是说,单例对象的实例化发生在类加载阶段或者应用程序启动时。
- 在饿汉式中,单例对象的实例在整个生命周期内都存在,并且可以被立即访问。
- 饿汉式实现相对简单,不存在线程安全问题,但在某些情况下可能造成不必要的资源浪费,因为实例被提前创建而不管是否被使用。
区别总结如下:
- 实例化时机:懒汉式是在需要时才进行实例化,而饿汉式是在程序启动时或类加载阶段就进行实例化。
- 延迟加载:懒汉式是延迟加载实例,只有在需要时才创建,而饿汉式是提前创建实例,立即可用。
- 线程安全性:懒汉式在多线程环境下需要考虑线程安全性,需要进行同步控制,而饿汉式不存在线程安全问题。
- 资源消耗:懒汉式避免了不必要的资源消耗,只有在需要时才创建实例,而饿汉式可能造成不必要的资源浪费,因为实例被提前创建而不管是否被使用。
选择使用懒汉式还是饿汉式取决于具体的应用场景和需求。如果资源消耗较大或需要延迟加载,懒汉式是一个较好的选择。如果资源消耗较小且需要立即可用,饿汉式是一个简单有效的方案。另外,还可以考虑其他的单例实现方式,如双重检查锁定、静态内部类等,以满足特定的需求。
二、饿汉式单例模式在程序启动时就创建实例
#pragma once class Singleton { private: Singleton(); Singleton(const Singleton &); Singleton& operator=(const Singleton &); ~Singleton(); public: static Singleton* getInstance(); private: static Singleton m_instance; };
#include "Singleton.h" #include <iostream> Singleton Singleton::m_instance; Singleton::Singleton() { std::cout << "创建饿汉式单例对象" << std::endl; } Singleton::Singleton(const Singleton&) { } Singleton& Singleton::operator=(const Singleton&) { return m_instance; } Singleton::~Singleton() { std::cout << "删除饿汉式单例对象" << std::endl; } Singleton* Singleton::getInstance() { return &m_instance; }
#include <iostream> #include "Singleton.h" int main() { system("pause"); return 0; }
打印结果:
三、懒汉式单例模式是在需要时才创建实例
#pragma once class Singleton { private: Singleton(); Singleton(const Singleton &); Singleton& operator=(const Singleton &); ~Singleton(); public: static Singleton* getInstance(); private: static Singleton *m_instance; };
#include "Singleton.h" #include <iostream> Singleton *Singleton::m_instance = nullptr; Singleton::Singleton() { std::cout << "创建懒汉式单例对象" << std::endl; } Singleton::Singleton(const Singleton&) { } Singleton& Singleton::operator=(const Singleton&) { return *m_instance; } Singleton::~Singleton() { std::cout << "删除懒汉式单例对象" << std::endl; } Singleton* Singleton::getInstance() { if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
#include <iostream> #include "Singleton.h" int main() { Singleton *sin1 = Singleton::getInstance(); Singleton* sin2 = Singleton::getInstance(); Singleton* sin3 = Singleton::getInstance(); return 0; }
打印结果:
可以看到,获取了三次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现。但是同样上述的单例模式也有两个问题:
1.线程安全的问题:当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来;
2.内存泄漏:注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。
因此,这里提供一个改进的,线程安全的、使用智能指针的实现;
#pragma once #include <memory> #include <mutex> class Singleton { private: Singleton(); Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: ~Singleton(); public: static std::shared_ptr<Singleton> getInstance(); private: static std::shared_ptr<Singleton> m_instance; static std::mutex m_mutex; };
#include "Singleton.h" #include <iostream> std::shared_ptr<Singleton> Singleton::m_instance = nullptr; std::mutex Singleton::m_mutex; Singleton::Singleton() { std::cout << "创建懒汉式单例对象" << std::endl; } Singleton::~Singleton() { std::cout << "删除懒汉式单例对象" << std::endl; } std::shared_ptr<Singleton> Singleton::getInstance() { if (m_instance == nullptr) { std::lock_guard<std::mutex> lock_mutex(m_mutex); if (m_instance == nullptr) { m_instance = std::shared_ptr<Singleton>(new Singleton()); } } return m_instance; }
#include <iostream> #include "Singleton.h" int main() { std::shared_ptr<Singleton> ptr1 = Singleton::getInstance(); std::shared_ptr<Singleton> ptr2 = Singleton::getInstance(); return 0; }
打印结果如下,发现确实只构造了一次实例,并且发生了析构
shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
- 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
- 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 getInstance的方法都加锁,锁的开销毕竟还是有点大的。
四、局部静态变量实现懒汉式单例
#pragma once class Singleton { private: Singleton(); Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; public: ~Singleton(); public: static Singleton& getInstance(); };
#include "Singleton.h" #include <iostream> Singleton::Singleton() { std::cout << "创建懒汉式单例对象" << std::endl; } Singleton::~Singleton() { std::cout << "删除懒汉式单例对象" << std::endl; } Singleton& Singleton::getInstance() { static Singleton instance; return instance; }
#include <iostream> #include "Singleton.h" int main() { Singleton& sin1 = Singleton::getInstance(); Singleton& sin2 = Singleton::getInstance(); return 0; }
打印结果:
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全;
- 不需要使用共享指针,代码简洁;
- 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。