学习软件设计,向OO高手迈进!
设计模式(Design pattern)是软件开发人员在软件开发过程中面临的一般问题的解决方案。
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
是前辈大神们留下的软件设计的"招式"或是"套路"。
什么是单例模式
定义:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例
单例模式有3个要点:
这个类只能有一个实例
它必须自己创建这个实例
它必须自己向整个系统提供这个实例
单例模式结构非常简单,只包含一个类,即单例类
为防止创建多个对象,其构造函数必须是私有的(外界不能访问)
为了提供一个全局访问点来访问该唯一实例,单例类会提供一个公有方法 getInstance() 来返回该实例
几种实现方式
有缺陷的懒汉式单例
懒汉式(Lazy-Initialization)方法是直到使用时才实例化对象,也就是说直到调用 getInstance() 方法的时
候才 new 一个单例对象,好处是如果不被调用就不会占用内存
#include <thread>
#include <stdio.h>
using namespace std;
class Singleton {
public:
~Singleton() {
printf("destructor called!\n");
}
static Singleton* getInstance() {
if (m_pSingle == nullptr) {
m_pSingle = new Singleton();
}
return m_pSingle;
}
private:
Singleton() {
printf("constructor called!\n");
}
Singleton(const Singleton &s) = delete; // 拷贝构造函数, 禁止使用
Singleton& operator=(const Singleton &s) = delete; // 赋值操作符, 禁止使用
static Singleton *m_pSingle;
};
// 必须在类外初始化
Singleton *Singleton::m_pSingle = nullptr;
void printAddress() {
// 获取实例
Singleton *singleton = Singleton::getInstance();
// 打印地址
printf("%p\n", singleton);
}
int main(int argc, char *argv[]) {
thread threads[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = thread(printAddress);
}
// 对每个线程调用join, 主线程等待子线程完成
for (int i = 0; i < 10; i++) {
threads[i].join();
}
return 0;
}
// build cmd: g++ -std=c++11 -pthread singleton.cpp
运行结果:
可以看出,实例被创建了3次(随机的)! 所以,这种单例模式不是线程安全的
除了线程不安全外,还有一个问题
-
线程安全的问题,当多线程获取单例时有可能引发竞态条件
第1个线程在 if 中判
m_pSingle
是空的,于是开始实例化单例同时第2个线程也尝试获取单例,这个时候判断
m_pSingle
还是空的,于是也开始实例化单这就会实例化出两个对象,这就是线程安全问题的由来。解决办法:加锁
-
内存泄漏
注意到类中只负责 new 出对象,却没有负责 delete 对象
因此只有构造函数被调用,析构函数却没有被调用
所以会导致内存泄漏。解决办法:使用共享指针
线程安全、内存安全的懒汉式单例
#include <thread>
#include <mutex>
#include <stdio.h>
using namespace std;
class Singleton {
public:
~Singleton() {
printf("destructor called!\n");
}
static shared_ptr<Singleton> getInstance() {
// "double checked lock" 双检锁
if (m_pSingle == nullptr) {
m_mutex.lock();
if (m_pSingle == nullptr) {
m_pSingle = shared_ptr<Singleton>(new Singleton());
}
m_mutex.unlock();
}
return m_pSingle;
}
private:
Singleton() {
printf("constructor called!\n");
}
Singleton(const Singleton &s) = delete; // 拷贝构造函数, 禁止使用
Singleton& operator=(const Singleton &s) = delete; // 赋值操作符, 禁止使用
static shared_ptr<Singleton> m_pSingle;
static mutex m_mutex;
};
// 必须在类外初始化
shared_ptr<Singleton> Singleton::m_pSingle = nullptr;
mutex Singleton::m_mutex;
void printAddress() {
// 获取实例
shared_ptr<Singleton> singleton = Singleton::getInstance();
// 打印智能指针的地址需要用 get() 方法
printf("%p\n", singleton.get());
}
int main(int argc, char *argv[]) {
thread threads[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = thread(printAddress);
}
// 对每个线程调用join, 主线程等待子线程完成
for (int i = 0; i < 10; i++) {
threads[i].join();
}
return 0;
}
// build cmd: g++ -std=c++11 -pthread singleton.cpp
运行结果如下,发现确实只构造了一次实例,并且发生了析构
shared_ptr 和 mutex 都是 C++11 的标准,以上这种方法的优点是
-
当 shared_ptr 析构的时候,new 出来的对象也会被 delete 掉。避免了内存泄漏
-
加了锁,使用互斥量来达到线程安全。这里使用了两个 if 判断语句的技术称为双检锁。好处是,只有判断指针为空的时候才加锁,避免每次调用 getInstance() 的方法都加锁,提高性能
不足之处在于
-
使用智能指针会要求用户代码也得使用智能指针
-
还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!具体可以看这篇文章,解释了为什么会发生这样的事情
因此这里还有第三种的基于 Magic Static 的方法达到线程安全
最推荐的懒汉式单例——局部静态变量
#include <thread>
#include <stdio.h>
using namespace std;
class Singleton {
public:
~Singleton() {
printf("destructor called!\n");
}
static Singleton& getInstance() {
// 局部静态变量
static Singleton pSingle;
return pSingle;
}
private:
Singleton() {
printf("constructor called!\n");
}
Singleton(const Singleton &s) = delete; // 拷贝构造函数, 禁止使用
Singleton& operator=(const Singleton &s) = delete; // 赋值操作符, 禁止使用
};
void printAddress() {
// 获取实例, 采用引用的形式
Singleton &singleton = Singleton::getInstance();
// 打印地址
printf("%p\n", &singleton);
}
int main(int argc, char *argv[]) {
thread threads[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = thread(printAddress);
}
// 对每个线程调用join, 主线程等待子线程完成
for (int i = 0; i < 10; i++) {
threads[i].join();
}
return 0;
}
// build cmd: g++ -std=c++11 -pthread singleton.cpp
运行结果
这种方法所用到的特性是在 C++11 标准中的 Magic Static 特性:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束
这样就保证了并发线程在获取局部静态变量的时候一定是初始化过的,所以具有线程安全性
C++ 静态变量的生存期是从声明到程序结束,这也是一种懒汉式
这是最推荐的一种单例实现方式:
- 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3 支持该特性)
- 不需要使用共享指针,使得代码简洁了
- 注意在使用的时候需要声明单例的引用
Singleton&
才能获取对象
另外,网上有人实现返回指针而不是返回引用
static Singleton* getInstance() {
static Singleton instance;
return &instance;
}
这样做并不好,理由主要是无法避免用户使用delete instance
导致对象被提前销毁。所以还是建议大家使用返回引用的方式!简单的理由来说是因为它足够简单却满足所有需求和顾虑
单例的模板
在某些情况下,我们系统中可能会有多个单例,如果都按照这种方式的话,实际上是一种重复,有没有什么方法可以只实现一次单例而能够复用其代码从而实现多个单例呢? 很自然我们会考虑使用模板技术或者继承的方法
#include <thread>
#include <stdio.h>
using namespace std;
template <typename T>
class Singleton {
public:
~Singleton() {
printf("destructor Singleton called!\n");
}
static T& getInstance() {
// 局部静态变量
static T pSingle;
return pSingle;
}
protected:
// 构造函数需要是 protected, 这样子类才能继承
Singleton() {
printf("constructor Singleton called!\n");
}
private:
Singleton(const Singleton &s) = delete; // 拷贝构造函数, 禁止使用
Singleton& operator=(const Singleton &s) = delete; // 赋值操作符, 禁止使用
};
class TestSingle : public Singleton<TestSingle> {
public:
// 需要将基类声明为友元, 这样基类才能调用子类的私有构造函数
friend class Singleton<TestSingle>;
~TestSingle() {
printf("destructor TestSingle called!\n");
}
private:
// 构造函数设为私有
TestSingle() {
printf("constructor TestSingle called!\n");
}
TestSingle(const TestSingle &s) = delete; // 拷贝构造函数, 禁止使用
TestSingle& operator=(const TestSingle &s) = delete; // 赋值操作符, 禁止使用
};
void printAddress() {
// 获取实例, 采用引用的形式
TestSingle &testSingle = TestSingle::getInstance();
// 打印地址
printf("%p\n", &testSingle);
}
int main(int argc, char *argv[]) {
thread threads[10];
// 创建10个线程
for (int i = 0; i < 10; i++) {
threads[i] = thread(printAddress);
}
// 对每个线程调用join, 主线程等待子线程完成
for (int i = 0; i < 10; i++) {
threads[i].join();
}
return 0;
}
// build cmd: g++ -std=c++11 -pthread singleton.cpp
运行结果
以上是实现一个单例的模板基类,子类需要将自己作为模板参数 T 传递给 Singleton
模板
同时需要将基类声明为友元,这样基类才能调用子类的私有构造函数
基类模板的实现要点是:
- 构造函数需要是 protected,这样子类才能继承
- getInstance() 也是采用推荐的局部静态变量的方法
优缺点
优点:
- 单例模式提供了严格的对唯一实例的创建和访问
- 单例模式的实现可以节省系统资源
缺点:
- 如果某个实例负责多重职责但又必须实例唯一,那单例类的职责过多,这违背了单一职责原则
- 多线程下需要考虑线程安全机制
适用环境:
- 系统只需要一个实例对象
- 某个实例只允许有一个访问接口