单例模式是一种创建型设计模式,用于确保一个类只有一个实例,并提供该实例的全局访问点。这种模式特别适用于控制资源访问,如日志处理、线程池管理、数据库连接等。
2. 特点和方法定义
单例模式主要包括以下几个特点:
- 唯一实例:确保只有一个实例存在。
- 全局访问:提供一个全局访问点供外部获取实例。
- 自我管理:自行创建并管理自己的唯一实例。
在类的设计中通常包含:
- 私有构造函数:防止外部通过
new
创建实例。 - 静态方法:通常为
getInstance()
,用于访问唯一的实例。 - 静态成员:用于持有自身的唯一实例。
- 删除拷贝构造函数和拷贝赋值运算符:防止实例被复制或赋值,以确保实例的唯一性。
单例模式可以分为 懒汉式 和 饿汉式 ,两者之间的区别在于创建实例的时间不同。
懒汉式
系统运行中,实例并不存在,只有当需要使用该实例时,才会去创建并使用实例。这种方式要考虑线程安全。
饿汉式
系统一运行,就初始化创建实例,当需要时,直接调用即可。这种方式本身就线程安全,没有多线程的线程安全问题
下面先实现基本的懒汉模式,代码如下:
实现一:单线程使用,多线程不安全
#include <iostream>
class Singleton {
public:
// 获取单例对象的静态方法
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
private:
Singleton() {} // 私有构造函数,防止外部通过new直接创建对象
Singleton(const Singleton&) = delete; // 删除拷贝构造函数,防止对象被拷贝
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符,防止对象被赋值
static Singleton* instance; // 静态成员变量
};
Singleton* Singleton::instance = nullptr; // 类外初始化静态成员变量
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << s1 << std::endl; // 输出对象地址
std::cout << s2 << std::endl; // 应该和s1相同,证明s1和s2是同一个实例
return 0;
}
由于没有了对象,所以将instance设置为static属性,让其能通过类名来访问获取。但是在多线程环境下,这种实现方式是不安全的,原因在于在判断instance是否为空时,可能存在多个线程同时进入if中,此时可能会实例化多个对象。——需要对变量加上互斥锁。
实现二:双重检查锁定的懒汉模式,实现代码如下:
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
// 首先检查实例是否已经创建,无需加锁
if (instance==nullptr) {
std::lock_guard<std::mutex> lock(mutex); // 加锁保护创建过程
// 在锁内部再次检查,以防在等待锁的时候实例被其他线程创建
if (instance==nullptr) {
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() {} // 私有构造函数,防止外部通过new直接实例化
Singleton(const Singleton&) = delete; // 禁止拷贝构造,防止复制实例
Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作,防止赋值实例
static Singleton* instance;
static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr; // 初始化静态成员变量
std::mutex Singleton::mutex;
int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();
std::cout << s1 << std::endl; // 输出对象地址
std::cout << s2 << std::endl; // 应该和s1相同,证明s1和s2是同一个实例
return 0;
}
双重锁检查机制通过两次检查实例是否已经被创建来避免不必要的锁定:
- 第一次检查(无锁状态):在任何锁操作之前,先检查实例是否已经被创建。这步操作不加锁,多个线程可以并发执行,如果实例已经存在,直接返回实例,避免了锁的开销。
- 加锁:如果第一次检查发现实例未创建,多个线程可能会尝试进入这个锁定区域。锁确保在任一时刻只有一个线程可以执行实例创建的代码。
- 第二次检查(加锁状态):即使进入锁定区域,线程还需要再次检查实例是否已经被创建。这是必需的,因为当第一个进入锁定区域的线程创建了实例并释放锁之后,后续进入的线程仍需要检查实例是否已经存在,以防止创建多个实例。
实现三:C++11后的更安全实现
从C++11开始,局部静态变量的初始化被标准规定为线程安全。可以用非常简洁的方式实现线程安全的单例模式,而无需使用互斥锁。
- 当第一个线程调用
getInstance()
并达到static Singleton instance;
时,它将开始创建Singleton
类的实例。 - 如果此时其他线程也调用
getInstance()
,它们将在instance
的构造完成之前被阻止进一步执行。 - 一旦
instance
被创建,所有线程都可以安全地访问这个已经初始化的实例。
#include <iostream>
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 静态局部变量,保证只初始化一次
return instance; // 返回单例对象的引用
}
private:
Singleton() {} // 私有构造函数,防止外部通过new直接实例化对象
Singleton(const Singleton&) = delete; // 禁止拷贝构造,防止对象被复制
Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作,防止对象赋值
};
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
std::cout << &s1 << std::endl; // 输出对象的地址
std::cout << &s2 << std::endl; // 应该与s1的输出地址相同
return 0;
}
懒汉模式
#include<iostream>
using namespace std;
//饿汉模式:不管用不用得到,都构造出来。本身就是线程安全的
class ehsingleClass {
public:
static ehsingleClass* getinstance()
{
return instance;
}
private:
static ehsingleClass* instance;//静态成员变量必须类外初始化,只有一个
ehsingleClass() {}
};
ehsingleClass* ehsingleClass::instance = new ehsingleClass();
//类外定义,main开始执行前,该对象就存在了
int main()
{
ehsingleClass* ehsinglep3 = ehsingleClass::getinstance();
ehsingleClass* ehsinglep4 = ehsingleClass::getinstance();
cout << ehsinglep3 << endl;
cout << ehsinglep4 << endl;
return 0;
}
总结
饿汉式即一种静态初始化的方式,它是类一加载就实例化对象,所以要提前占用系统资源。而懒汉式又面临着多线程不安全的问题,需要加二重锁才能保证安全,因此具体使用哪种模式,需要根据实际需求和场景来定。
上面讨论的线程安全指的是getInstance()
是线程安全的,假如多个线程都获取类A
的对象,如果只是只读操作,没有问题,但是如果有线程要修改,有线程要读取,那么类A
自身的函数需要自己加锁防护,不是说线程安全的单例也能保证修改和读取该对象自身的资源也是线程安全的。