单例模式讲解
在C 中,单例模式是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式对于管理资源、控制对共享资源的访问或者创建一些中心控制类非常有用例如相关配置管理器、日志记录器、数据库连接池等。
单例模式的基本特点:
- 私有构造函数:防止其他对象通过构造函数创建该类的实例。
- 静态成员函数:提供一个全局访问点用于返回该类的唯一实例。
- 静态成员变量:保存该类的唯一实例。
实现步骤:
- 声明单例类:定义类并将其构造函数相关设置为私有,这是为了防止产生多个单例
- 创建静态成员变量:声明一个静态成员变量来存储唯一的实例。
- 提供获取实例的方法:定义一个静态成员函数,用于返回单例的实例,也就是该单例的全局访问点。
通常在需要使用该类的.cpp文件引入该单例类所在的头文件,并用全局访问点访问就行了
用懒汉模式还是饿汉模式?
懒汉模式
- 定义:在第一次请求时创建单例实例。
- 优点:节省资源,因为只有在需要时才创建实例。
- 缺点:需要额外的同步机制例如锁来保证线程安全。
GetInstance()方法:这个静态成员函数检查_instance是否已经被初始化,如果没有,它创建一个新的单例实例。然后返回该实例的引用。
很明显只有第一次请求时会创造并初始化单例实例,另外记得在类外定义类内声明的静态成员变量
非线程安全的简单使用例:
代码语言:cpp复制#include<iostream>
class Singleton {
public:
static Singleton& GetInstance() {
if (!_instance) {
_instance = new Singleton();
}
return *_instance;
}
void DoSomething() {
std::cout << "Doing something useful.n";
}
void AddCount()
{
count = 1;
}
int GetCount()
{
return count;
}
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构函数
Singleton(const Singleton&) = delete; // 删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符
static Singleton* _instance;
int count = 0;
};
//类外定义
Singleton* Singleton::_instance = nullptr;
int main() {
Singleton& s = Singleton::GetInstance();//全局访问点
s.DoSomething();
std::cout << Singleton::GetInstance().GetCount() << std::endl;
return 0;
}
当多线程时,为什么需要额外的同步机制?
这是因为不加锁的话,多个线程可能在第一次初始化时创造出多个单例对象造成线程安全问题
通常可以使用锁或者call_once等等来同步
线程安全的简单使用例:
以call_once为例
代码语言:cpp复制#include <iostream>
#include <mutex>
#include <thread>
class Singleton {
public:
static Singleton& GetInstance() {
std::call_once(_onceFlag, &Singleton::initInstance);
return *_instance;
}
void DoSomething() {
std::cout << "Doing something useful.n";
}
void Add()
{
count = 1;
}
int GetCount()
{
return count;
}
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构函数
Singleton(const Singleton&) = delete; // 删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符
static Singleton* _instance;
static std::once_flag _onceFlag;
int count = 0;
static void initInstance() { // 静态成员函数
_instance = new Singleton();
}
};
// 类外定义
Singleton* Singleton::_instance = nullptr;
std::once_flag Singleton::_onceFlag;
int main() {
Singleton& s = Singleton::GetInstance(); // 全局访问点
s.DoSomething();
s.Add();
std::cout << "count:" <<Singleton::GetInstance().GetCount() <<std::endl;
return 0;
}
这样,无论有多少个线程试图获取 Singleton 的实例,只会有一个线程创建该实例,其他线程将等待直到 _instance 被初始化完成。
多线程下的线程安全简单使用例
代码语言:cpp复制#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
class Singleton {
public:
static Singleton& GetInstance() {
std::call_once(_onceFlag, &Singleton::initInstance);
return *_instance;
}
void DoSomething() {
std::cout << "Doing something useful.n";
}
void Add() {
std::lock_guard<std::mutex> lock(_mutex); // 使用锁保护对count的访问
count = 1;
}
int GetCount() const {
return count;
}
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构函数
Singleton(const Singleton&) = delete; // 删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符
static Singleton* _instance;
static std::once_flag _onceFlag;
mutable std::mutex _mutex; // 用于同步对count的访问
int count = 0;
static void initInstance() { // 静态成员函数
_instance = new Singleton();
}
};
// 类外定义
Singleton* Singleton::_instance = nullptr;
std::once_flag Singleton::_onceFlag;
void incrementCount(Singleton& singleton, int times) {
for (int i = 0; i < times; i) {
singleton.Add();
}
}
int main()
{
Singleton& s = Singleton::GetInstance(); // 全局访问点
s.DoSomething();
// 创建两个线程来增加count
std::thread t1(incrementCount, std::ref(s), 5000);
std::thread t2(incrementCount, std::ref(s), 5000);
// 等待线程完成
t1.join();
t2.join();
std::cout << "count: " << s.GetCount() << std::endl;
return 0;
}
运行截图
饿汉模式
- 定义:在程序启动时就创建单例实例。
- 优点:无需额外的同步机制,因为实例在编译时就已经创建。
- 缺点:即使从未使用过单例,也会占用资源。
与懒汉模式的主要区别体现在全局访问点函数以及无需在类内声明静态成员变量
简单的使用例
代码语言:cpp复制#include <iostream>
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton instance; // 静态局部变量
return instance;
}
void DoSomething()
{
std::cout << "Doing something useful.n";
}
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构函数
Singleton(const Singleton&) = delete; // 删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符
};
int main() {
Singleton& s = Singleton::GetInstance();//全局访问点
s.DoSomething();
return 0;
}
全局访问点返回引用还是指针?
为控制变量
以下均基于饿汉模式下的单例模式讲解
返回值是引入的好处
代码语言:cpp复制 static Singleton& GetInstance()
{
static Singleton instance; // 静态局部变量
return instance;
}
- 更高效: 返回引用不需要额外的指针间接寻址,因此通常更高效。
- 更安全: 返回引用可以防止意外的空指针解引用错误。
返回值是指针的好处
代码语言:cpp复制 static Singleton* GetInstance()
{
static Singleton instance; // 静态局部变量
return &instance;
}
- 使用指针可以有更多的灵活性,比如可以在某些情况下返回
nullptr
。
结论
选择懒汉模式还是饿汉模式取决于具体需求。如果希望节省资源并且能够接受额外的同步开销,则选择懒汉模式;如果希望在程序启动时就准备好所有资源,则选择饿汉模式。
至于返回引用还是指针的选择,通常返回引用更为推荐,因为它更安全且避免了不必要的开销。但有时,返回指针会更灵活。