定义
一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
实战案例
处理资源访问冲突
自定义实现一个往文件中打印日志的 Logger 类。
表示全局唯一类
从业务上讲,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器设计为单例。
实现一个单例
要实现一个单例,我们需要关注的点无外乎下面几个:
构造函数需要是 private 访问权限,这样才能避免外部创建实例;
考虑对象创建时的线程安全问题;
考虑是否支持延迟加载;
考虑
getInstance()
性能是否高(是否加锁)。
饿汉式
在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(真正用到的时候,再创建实例)。
#include <new>
class Singleton
{
public:
Singleton(const Singleton& single) = delete;
Singleton& operator=(const Singleton& single) = delete;
static Singleton* GetInstance();
private:
Singleton() = default;
~Singleton() = default;
static Singleton* m_pInstance;
class GC
{
public:
~GC()
{
if (m_pInstance != nullptr)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
static GC gc;
};
Singleton* Singleton::m_pInstance = new (std::nothrow) Singleton;
Singleton::GC Singleton::gc;
Singleton* Singleton::GetInstance()
{
return m_pInstance;
}
int main(int argc, char* argv[])
{
Singleton* object = Singleton::GetInstance();
return 0;
}
有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方式应该是用到的时候再去初始化。不过,我个人并不认同这样的观点。
如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴漏),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
懒汉式
有饿汉式,对应的,就有懒汉式。懒汉式相对于恶汉式的优势是支持延迟加载。
普通懒汉式(线程不安全)
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* GetInstance()
{
if(m_pInstance == nullptr)
{
m_pInstance = new (std::nothrow) Singleton();
}
return m_pInstance;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton *m_pInstance;
class GC
{
public:
~GC()
{
if(m_pInstance != nullptr)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
static GC gc;
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::GC Singleton::gc;
加锁的懒汉式单例(线程安全)
class Singleton
{
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* GetInstance()
{
if(m_pInstance == nullptr)
{
std::unique_lock<std::mutex> lock(m_mutex);
if(m_pInstance == nullptr)
{
m_pInstance = new (std::nothrow) Singleton();
}
}
return m_pInstance;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton *m_pInstance;
class GC
{
public:
~GC()
{
if(m_pInstance != nullptr)
{
delete m_pInstance;
m_pInstance = nullptr;
}
}
};
static GC gc;
static std::mutex m_mutex;
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::GC Singleton::gc;
std::mutex Singleton::m_mutex;
实际上,cpu 指令重排序可能会导致问题。
p = new Singleton;
这条语句会导致三件事情的发生:
分配能够存储
Singleton
对象的内存;在被分配的内存中构造一个
Singleton
对象;让
p
指向这块被分配的内存。
一般情况下执行顺序是 123,但是指令重排序下执行顺序可能会变为 132。这种情况下,会导致线程 A 执行了 3,但是 2 还未执行,线程 B 判断 p 非空,但是实际上对象还未构造。
不过这个问题在 C++11 中得到了解决,因为新的 C++11 规定了新的内存模型,保证执行上述 3 个步骤的时候不会发生线程切换,相当于这个初始化过程是"原子性"的操作(后续补充)。
内部静态变量的懒汉式
class Singleton
{
public:
Singleton(const Singleton& single) = delete;
Singleton& operator=(const Singleton& single) = delete;
static Singleton* get_instance()
{
static Singleton instance;
return &instance;
}
private:
Singleton() = default;
~Singleton() = default;
};
int main()
{
Singleton* object = Singleton::get_instance();
return 0;
}
单例模式宏
#define SINGLETON_DEFINE(TypeName) \
static TypeName* GetInstance() \
{ \
static TypeName type_instance; \
return &type_instance; \
} \
TypeName(const TypeName&) = delete; \
TypeName& operator=(const TypeName&) = delete; \
单例存在哪些问题
单例对 OOP 特性的支持不友好
通过 IdGenerator 这个例子来讲解
class Order {
public:
void create(...) {
// ...
long id = IdGenerator.GetInstance().GetId();
// ...
}
};
class User {
public:
void create(...) {
// ...
long id = IdGenerator.GetInstance().GetId();
// ...
}
};
IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,我们希望针对不同业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码改动就会比较大。
class Order {
public:
void create(...) {
// ...
long id = OrderIdGenerator.GetInstance().GetId();
}
};
public User {
public:
void create(...) {
long id = UserIdGenerator.GetInstance().GetId();
}
};
除此之外,单例对继承、多态特性的支持也不友好。
单例会隐藏类之间的依赖关系
单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。在阅读代码的时候,我们就需要仔细查看每个函数的代码实现,才能知道这个类到底依赖了哪些单例类。
单例对代码的扩展性不友好
单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。
比如,数据库连接池,需要区分慢 SQL 和其他 SQL,我们希望可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。
单例不支持有参数的构造函数
解决方案:
将参数放到另外一个全局变量中。Config 是一个存储了 paramA 和 paramB 值的全局变量。里面的值既可以通过静态常量定义,也可以额从配置文件中加载得到。
struct Config {
const static int PARAM_A = 123;
const static int PARAM_B = 245;
};
class Singleton {
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* GetInstance() {
static Singleton instance;
return &instance;
}
private:
Singleton()
: m_paramA(Config::PARAM_A)
, m_paramB(Config::PARAM_B)
{}
~Singleton() = default;
int m_paramA;
int m_paramB;
};
int main() {
Singleton* object = Singleton::GetInstance();
return 0;
}
单例的替代方案
为了保证全局唯一,除了使用单例,还可以用静态方法来实现。
#include <atomic>
#include <iostream>
class IdGenerator {
public:
static long GetId() {
return id.fetch_add(1);
}
private:
static std::atomic_long id;
};
std::atomic_long IdGenerator::id = 0;
int main() {
std::cout << IdGenerator::GetId() << std::endl;
}
不过,静态方法这种实现思路,并不能解决我们之前提到的问题。实际上,它比单例更加不灵活,比如,无法支持延迟加载。我们再来看看有没有其他办法。
// 依赖注入
void DemoFunction(IdGenerator* idGenerator) {
long id = idGenerator->GetId();
}
IdGenerator* idGenerator = IdGenerator::GetInstance();
DemoFunction(idGeneator);
基于新的使用方式,我们将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间的依赖关系的问题。不过,对于单例存在的其他问题,比如对 OOP 特性、扩展性不友好等问题,还是无法解决。
总结
如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。
参考:王争 设计模式之美