单例模式可能是最简单,最常用的设计模式之一,这篇文章主要总结一些常用的实现方式
单例是什么
单例模式(Singleton), 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
应用场景
当我们需要统一管理资源或者资源共享的时候都可以使用单例模式
- 管理资源:多线程/数据库 线程池,资源管理器等等
- 资源共享:读写应用配置,日志应用等等
代码实现说明
- 为了保证全局只有一个实例,需要使用 static, 同时需要防止被外界创建和赋值,就需要私有化它的构造函数,并且禁止赋值和拷贝
- 提供一个公有的静态方法获取该实例
而关于什么时候初始化这个单例,实际上有分为2种
类型 | 描述 |
---|---|
饿汉模式 | 变量在声明时便初始化 |
懒汉模式 | 先声明一个空变量,用户调用的时候才初始化 |
饿汉模式
饿汉模式 像一个饿汉一样,不管需不需要用到实例都要去创建实例,变量在声明时便初始化,所以可以有以下的实现方法
class EagerSingleton
{
public:
static EagerSingleton& getInstance(){return instance;}
private:
EagerSingleton() { std::cout << "EagerSingleton create" << std::endl; }
~EagerSingleton(){}
EagerSingleton(const EagerSingleton&)=delete;
EagerSingleton& operator=(const EagerSingleton&)=delete;
private:
static EagerSingleton instance;
};
EagerSingleton EagerSingleton::instance;
将构造,赋值,复制,析构都设置成私有函数,暴露一个 getInstance
接口,返回静态成员对象 instance
供开发人员调用
当然这里静态变量 instance
也可以设计成指针, 只要保证在声明时初始化即可例如
class EagerSingleton
{
public:
static EagerSingleton* getInstance(){return instance;}
...
private:
static EagerSingleton* instance;
};
EagerSingleton* EagerSingleton::instance = new EagerSingleton;
由于在 main()
函数之前初始化,所以没有线程安全的问题,但是这里存在2个可能存在问题
过早的初始化
因为从 main()
函数之前就初始化,但是程序可能全程没有用到 EagerSingleton
这个类,可以完全不用这么早初始化,等用的时候在初始化就可以
之前看 《More Effective C++》
第17条款的时候,提到过 lazy evaluation(缓式评估)
, 从效率而言,最好的运算是从未被执行的运算,如果没用到,完全可以不去做,能拖就拖,拖到必须做,必须迫切需要的时候再去执行,这就引入了单例的另一种实现–懒汉模式
non-local static 引入的潜在风险
先看看什么是 non-local static
下面内容摘自 《Effective C++》
的条款04
函数内的
static
对象称为local static
对象(因为它们对函数而言是 local),其他static
对象被称为non-local static
如果某个编译单元内某个 non-local static
对象的初始化动作使用了另一个编译单元的某个 non-local static
对象,它所用到的这个对象可能尚未被初始化, 因为 C++ 对 “定义于不同编译单元内的 non-local static
的初始化次序并无明确定义”, 主要是决定它们的初始化顺序相当困难,根本无解
当然这种情况在实际编写代码的时候可能很少出现,并且书中也提出一个小小的设计来完全消除这个问题
以 local static 对象替换 non-local static 对象
具体关于这种设计的说明,后面代码里有介绍,这里就先不展开了
懒汉模式
懒汉模式,像一个懒汉一样,需要用到该对象的时候再去创建实例,不需要创建实例程序就“懒得”去创建实例,相对于饿汉模式还没开始(mian函数)就初始化显得更加合理
从未被调用,就不用承担构造和析构的成,其实换句话可以这样理解
- 饿汉模式是拿空间换时间做法
- 懒汉模式是拿时间换空间做法
基础版
class LazySingletonDemo
{
private:
static LazySingletonDemo* instance;
private:
LazySingletonDemo() { std::cout << "LazySingletonDemo create" << std::endl;}
~LazySingletonDemo() {}
LazySingletonDemo(const LazySingletonDemo&)=delete;
LazySingletonDemo& operator=(const LazySingletonDemo&)=delete;
public:
static LazySingletonDemo* getInstance()
{
if(instance == nullptr)
instance = new LazySingletonDemo();
return instance;
}
};
LazySingletonDemo* LazySingletonDemo::instance = nullptr;
调用的时候才初始化,程序单线程调用时是没有问题的,但是多线程系统中就存在不确定性了,可能因为多个线程同时请求初始化导致的内存泄漏问题
所以问题来到了如何消除与初始化有关的 “竞速形势”
最简单的就是初始化前加锁,保证创建这个过程是唯一的,好,就有下面这种改进版
双检查锁 Double-Checked Locking Pattern
#include <iostream>
#include <mutex>
class LazySingletonMore
{
private:
static LazySingletonMore* instance;
static std::mutex mutex;
private:
LazySingletonMore() { std::cout << "LazySingletonMore create" << std::endl;}
~LazySingletonMore() {}
LazySingletonMore(const LazySingletonMore&)=delete;
LazySingletonMore& operator=(const LazySingletonMore&)=delete;
public:
static LazySingletonMore* getInstance()
{
if(instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
if(instance == nullptr)
instance = new LazySingletonMore();
}
return instance;
}
};
LazySingletonMore* LazySingletonMore::instance = nullptr;
std::mutex LazySingletonMore::mutex;
- 第一步判断,如果已经创建了,直接返回
- 第二步加锁,如果多个线程同时进入了初始化环节,加锁保证同步线程
- 拿到锁的线程,因为未初始化,创建实例,创建结束,同步结束
- 同步结束后,其他未执行初始化的线程,因为已经初始化了,就不用再创建了。
C++11推荐方式
#include <iostream>
using namespace std;
class Singleton
{
public:
static Singleton& getInstance()
{
static Singleton value;
return value;
}
private:
Singleton() = default;
Singleton(const Singleton& other)=delete;
Singleton& operator=(const Singleton&)=delete;
};
这个手法的基础在于: C++保证,函数中的 local static
对象会在 “该函数被调用期间” “首次遇上该对象之定义式” 时被初始化,所以如果以 函数调用 返回一个 reference
指向 local static
对象 替换 直接访问 non-local static
对象,就可以获得保证,保证所获得的那个 reference
将指向一个 历经初始化 的对象
不过 local static
线程安全是 C++11 之后才支持的,在 C++11 之前,在多线程环境下local static
对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行 local static
对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该 local static
对象的构造函数中。这会造成这个 local static
对象的重复构造,进而产生 内存泄露 问题。
总结
虽然很常用,但是在保证线程安全的前提下,实现起来考虑的细节还是挺多的,
还有一些细节的知识,建议看一下下面几篇博客,里面提了一些很有意思的思考
- 通过C++11提供的 call_once 来实现单例
getInstance
到底应该返回指针还是引用- 如果想复用的话,利用模板应该如何实现等等
https://www.cnblogs.com/loveis715/archive/2012/07/18/2598409.html
https://zhuanlan.zhihu.com/p/37469260
https://zhuanlan.zhihu.com/p/62014096
关于单例的总结大概也就这么多了