单例模式(Singleton Pattern)是 面向对象中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
优点:通过单例模式的设计,使得创建的类在当前进程中只有一个实例,并提供一个全局性的访问点,这样可以规避因频繁创建对象而导致的 内存飙升 情况。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
在面向对象编程中,有时候我们希望一个类只有一个实例化的对象,比如线程池,缓存等。这些类有且只有一个唯一的实例,这种设计模式被称为单例模式。
1)私有化构造函数:这样外界就无法自由地创建类对象,进而阻止了多个实例的产生。
2)类定义中含有该类的唯一静态私有对象:静态变量存放在全局存储区,且是唯一的,供所有对象使用。
3)用公有的静态函数来获取该实例:提供了访问接口。
单例模式有两种主要实现方法:懒汉模式和饿汉模式。
懒汉模式特点是当外界调用时才进行实例化。
是否多线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
缺点:一个是线程安全,另一个是内存泄漏。
线程安全是因为在多线程场景下,有可能出现多个线程同时进行new操作的情况,没通过加锁来限制。
内存泄漏是因为使用了new在堆上分配了资源,那么在程序结束时,也应该进行delete,确保堆中数据释放。
public class Singleton {
// 静态私有对象
private static Singleton instance;
// 私有构造函数
private Singleton (){}
// 公有接口获取唯一实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式在单线程下没有问题,但是如果多线程模式下,当唯一实例还没有创建,两个线程同时调用getinstance就可能同时创建对象,导致错误。
是否多线程安全:是
实现难度:较易
描述:这种方式采用单锁机制,有可能造成阻塞。
Singleton*Singleton::getInstance(){
m.lock();
if (_instance == nullptr)
_instance = new Singleton;
m.unlock();
return _instance;
}
加锁又会带来另外的性能问题,如果每个线程每次获取实例都加锁,有可能造成阻塞的发生。实际上,上锁的目的是为了防止有多个线程在实例未被初始化的情况下,同时对他进行初始化,如果实例已经被创建了,就不需要考虑这个问题了,所以就可以采用二次加锁的方法来提高程序的性能。
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,可以确保线程安全,且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
Singleton*Singleton::getInstance(){
if (_instance == nullptr)
{
m.lock();
if (_instance == nullptr)
{
_instance = new Singleton;
}
m.unlock();
}
return _instance;
}
接下来,我们再解决内存泄漏(资源释放)问题,对懒汉式实现进行进一步的改进。
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,可以确保线程安全,且在多线程情况下能保持高性能。并且加入资源管理机制,以达到对资源的释放的目的。
我们加入资源管理机制,以达到对资源的释放的目的,解决方法有两个:智能指针&静态嵌套类。
将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static shared_ptr<Singleton> getInstance() {
// 若为空则创建
if (instance == nullptr) {
// 加锁保证线程安全
// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
lock_guard<mutex> l(m_mutex);
if (instance == nullptr) {
cout << "实例为空,开始创建。" << endl;
instance.reset(new Singleton(), destoryInstance);
cout << "地址为:" << instance << endl;
cout << "创建结束。" << endl;
}
}
else {
cout << "已有实例,返回。" << endl;
}
return instance;
}
// 毁灭实例
static void destoryInstance(Singleton* x) {
cout << "自定义释放实例" << endl;
delete x;
}
private:
// 私有构造函数
Singleton() {
cout << "构造函数启动。" << endl;
};
// 私有析构函数
~Singleton() {
cout << "析构函数启动。" << endl;
};
private:
// 静态私有对象
static shared_ptr<Singleton> instance;
// 锁
static mutex m_mutex;
};
// 初始化
shared_ptr<Singleton> Singleton::instance;
mutex Singleton::m_mutex;
应用智能指针后,在程序结束时,它自动进行资源的释放,解决了内存泄漏的问题。
类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton* getInstance() {
// 若为空则创建
if (instance == nullptr) {
// 加锁保证线程安全
// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
lock_guard<mutex> l(m_mutex);
if (instance == nullptr) {
cout << "实例为空,开始创建。" << endl;
instance = new Singleton();
cout << "地址为:" << instance << endl;
cout << "创建结束。" << endl;
}
}
else {
cout << "已有实例,返回。" << endl;
}
return instance;
}
private:
// 私有构造函数
Singleton() {
cout << "构造函数启动。" << endl;
};
// 私有析构函数
~Singleton() {
cout << "析构函数启动。" << endl;
};
// 定义一个删除器
class Deleter {
public:
Deleter() {};
~Deleter() {
if (instance != nullptr) {
cout << "删除器启动。" << endl;
delete instance;
instance = nullptr;
}
}
};
// 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
static Deleter m_deleter;
private:
// 静态私有对象
static Singleton* instance;
// 锁
static mutex m_mutex;
};
// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
Singleton::Deleter Singleton::m_deleter;
是否多线程安全:是
实现难度:一般
描述:C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的。
因为有上述的改动,所以出现了一种更简洁方便优雅的实现方法,基于局部静态对象实现。
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton& getInstance() {
cout << "获取实例" << endl;
static Singleton instance;
cout << "地址为:" << &instance << endl;
return instance;
}
private:
// 私有构造函数
Singleton() {
cout << "构造函数启动。" << endl;
};
// 私有析构函数
~Singleton() {
cout << "析构函数启动。" << endl;
};
};
饿汉模式特点是一开始就对实例进行初始化,调用时直接返回这个构建好的实例。
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。第一次调用才初始化,避免内存浪费。
缺点:类加载时就初始化,浪费内存。必须加锁 synchronized 才能保证单例,但加锁会影响效率。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
#include <iostream>
#include <mutex>
using namespace std;
// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton* getInstance() {
cout << "获取实例" << endl;
cout << "地址为:" << instance << endl;
return instance;
}
private:
// 私有构造函数
Singleton() {
cout << "构造函数启动。" << endl;
};
// 私有析构函数
~Singleton() {
cout << "析构函数启动。" << endl;
};
private:
// 静态私有对象
static Singleton* instance;
};
// 初始化
Singleton* Singleton::instance = new Singleton();
main还没开始,实例就已经构建完毕,获取实例的函数也不需要进行判空操作,因此也就不用双重检测锁来保证线程安全了,它本身已经是线程安全状态了。
但是内存泄漏的问题还是要解决的,这点同懒汉是一样的。可以通过智能指针和静态嵌套实现。
一般情况下,建议使用基于双重检测锁和资源管理搭配智能指针的懒汉方式。