C++设计模式-单例模式
文章目录
一、概念
单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例:
- 保证一个类仅有一个实例
- 提供一个访问它的全局访问点
- 该实例被所有程序模块所共享
二、应用场景
单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接等需要单线程操作的场合,用于实现对系统资源的控制。
三、定义方式
- 私有化构造函数,以防止外界创建单例类的对象
- 不需用拷贝和赋值,在单例模式中,始终只有一个对象
- 提供一个自身的静态私有成员变量,以指向类的实例
- 使用一个公有的静态方法获取该实例
四、实现方式
- 解法一:基础懒汉式单例——单线程安全,多线程不安全
- 解法二:加锁懒汉式单例——效率低
- 解法三:对解法二的一个优化
- 解法四:饿汉式单例——线程安全
- 解法五:利用C++11的magic static特性实现的Meyers Singleton,安全,推荐
4.1 基础懒汉式单例
示例:
#include <iostream>
/**
* @brief 基本懒汉式单例(非线程安全)
* 由于要求只生成一个实例,因此必须把构造函数设为私有函数,以禁止他人创建实例
* 可以定义一个静态的实例,在需要的时候创建该实例
* 缺点:多线程情况下,不同线程可能创建出不同的Singleton实例
*/
class Singleton {
public:
static Singleton* getInstance() {
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
static void destroyInstance() {
if (instance_ != nullptr) {
delete instance_;
instance_ = nullptr;
}
}
void todo() { std::cout << "Singleton do ..." << std::endl; }
private:
Singleton() = default; // 默认构造
~Singleton() = default; // 默认析构
Singleton(const Singleton&) = delete; // 删除拷贝构造
Singleton& operator=(const Singleton&) = delete; // 删除拷贝赋值
Singleton(Singleton&&) = delete; // 删除移动构造
Singleton& operator=(Singleton&&) = delete; // 删除移动赋值
private:
static Singleton* instance_;
};
// 初始化
Singleton* Singleton::instance_ = nullptr;
// test
int main() {
Singleton::getInstance()->todo();
return 0;
}
注意:此懒汉式单例,在多线程时,可能会存在多个线程同时getInstance(),导致创建多个不同的实例。
4.2 加锁懒汉式单例
实例:
#include <iostream>
#include <mutex>
/**
* @brief 加锁懒汉式单例
* 由于要求只生成一个实例,因此必须把构造函数设为私有函数,以禁止他人创建实例
* 通过加一个同步锁来保证线程安全
* 缺点:加锁是一个非常耗时的操作,会影响效率
*/
class Singleton {
public:
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mutex_); // 加锁
if (instance_ == nullptr) {
instance_ = new Singleton();
}
return instance_;
}
static void destroyInstance() {
if (instance_ != nullptr) {
delete instance_;
instance_ = nullptr;
}
}
void todo() { std::cout << "Singleton do ..." << std::endl; }
private:
Singleton() = default; // 默认构造
~Singleton() = default; // 默认析构
Singleton(const Singleton&) = delete; // 删除拷贝构造
Singleton& operator=(const Singleton&) = delete; // 删除拷贝赋值
Singleton(Singleton&&) = delete; // 删除移动构造
Singleton& operator=(Singleton&&) = delete; // 删除移动赋值
private:
static Singleton* instance_;
static std::mutex mutex_;
};
// 初始化
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
// test
int main() {
Singleton::getInstance()->todo();
return 0;
}
注意:每次getInstance()时都会加锁,影响效率
4.3 对解法二的一个优化
示例:
class Singleton {
public:
static Singleton* getInstance() {
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(mutex_); // 先判断实例为nullptr后再加锁
if (instance_ == nullptr) { // 再次判断
instance_ = new Singleton();
}
}
return instance_;
}
static void destroyInstance() {
if (instance_ != nullptr) {
delete instance_;
instance_ = nullptr;
}
}
void todo() { std::cout << "Singleton do ..." << std::endl; }
private:
Singleton() = default; // 默认构造
~Singleton() = default; // 默认析构
Singleton(const Singleton&) = delete; // 删除拷贝构造
Singleton& operator=(const Singleton&) = delete; // 删除拷贝赋值
Singleton(Singleton&&) = delete; // 删除移动构造
Singleton& operator=(Singleton&&) = delete; // 删除移动赋值
private:
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
// 初始化
std::atomic<Singleton*> Singleton::instance_(nullptr);
std::mutex Singleton::mutex_;
// test
int main() {
Singleton::getInstance()->todo();
return 0;
}
注意:在getInstance()中先做指针为空的判断,再进行加锁,可以避免每次都加锁,解决效率问题。
但是由于内存reorder可能导致线程并不安全(编译器的问题),new Singleton() 被分成三个步骤执行:
- 分配一个Singleton类型对象所需的内存
- 在分配的内存初始化Singleton类型对象
- 把分配的内存的地址赋给指针instance_
这里步骤1是肯定最先执行的,由但于编译器的问题(C++98),可能会发生 new 操作指令内存reorder,导致步骤2和步骤3的顺序并不一定;假如线程A的执行顺序是 1,3,2,在执行到步骤3时,此时instance_不再为空(nullptr),这时就切换到了线程B,而由于instance_不为空,所以线程B会直接返回instance_,而不会再次创建实例,但这个Singleton类型对象并没有真正被构造,此时如果线程B去访问示例内部的数据时就会出现问题。
解决这个问题,对于C++,在C++11标准中,可以使用原子操作,即atomic。
static std::atomic<Singleton*> instance_;
4.4 饿汉式单例
示例:
#include <iostream>
/**
* @brief 饿汉式单例(线程安全)
* 程序刚开始运行时就立即进行初始化,也称为预初始化
* 缺点:在程序运行时,会占用内存,影响程序性能
*/
class Singleton {
public:
static Singleton* getInstance() {
return instance_;
}
static void destroyInstance() {
if (instance_ != nullptr) {
delete instance_;
instance_ = nullptr;
}
}
void todo() { std::cout << "Singleton do ..." << std::endl; }
private:
Singleton() = default; // 默认构造
~Singleton() = default; // 默认析构
Singleton(const Singleton&) = delete; // 删除拷贝构造
Singleton& operator=(const Singleton&) = delete; // 删除拷贝赋值
Singleton(Singleton&&) = delete; // 删除移动构造
Singleton& operator=(Singleton&&) = delete; // 删除移动赋值
private:
static Singleton* instance_;
};
// 初始化
Singleton* Singleton::instance_ = new Singleton();
// test
int main() {
Singleton::getInstance()->todo();
return 0;
}
注意:在程序开始运行时就申请了内存,占用系统资源
4.5 Meyers Singleton,推荐
利用局部静态变量来实现单例模式。最早由 C++ 大佬、Effective C++ 系列的作者 Scott Meyers 提出,因此也被称为 Meyers’ Singleton
- 最简洁:不需要额外定义类的静态成员
- 线程安全,不需要额外加锁
- 没有烦人的指针
这是 C++11 之后的单例模式最佳实践。
#include <iostream>
/**
* @brief 利用C++11的 magic static 特性,称为Meyers Singleton,即简单又安全的单例模式实现
* 核心思想是:利用局部静态变量的特性,来保证单例的延迟初始化和线程安全
* 静态局部变量,只有在首次定义定义它的函数时才被构造,之后的调用都直接跳过,返回同一实例
* C++11 标准规定,局部静态变量的初始化过程时线程安全的,这意味着在多线程下,局部静态变量的构造只会执行一次,并且在多线程访问时,也是安全的
* 如果构造函数异常,局部静态的析构函数也会被执行,保证了单例的完整性
*
* 优点:
* 延迟加载
* 系统自动调用析构,回收内存
* 没有加锁和new操作,实现简单,效率高
* 线程安全
*
* gcc4.0及之后的版本支持
* C++11及之后的标准支持
*/
class Singleton {
public:
static Singleton& getInstance() {
// 局部静态变量,只有在首次定义定义它的函数时才被构造,之后的调用都直接跳过,返回同一实例
static Singleton instance;
return instance;
}
void todo() { std::cout << "Singleton do ..." << std::endl; }
private:
Singleton() = default; // 默认构造
~Singleton() = default; // 默认析构
Singleton(const Singleton&) = delete; // 删除拷贝构造
Singleton& operator=(const Singleton&) = delete; // 删除拷贝赋值
Singleton(Singleton&&) = delete; // 删除移动构造
Singleton& operator=(Singleton&&) = delete; // 删除移动赋值
};
// test
int main() {
Singleton::getInstance().todo();
return 0;
}
总结
以上列出了几种常见的单例模式方法和实现,推荐使用最后一种,即Meyers Singleton