一、什么是单例模式?
单例模式是一种设计模式,确保一个类在整个程序中只有一个实例对象,并提供一个全局访问点来获取这个实例。
通俗理解:
-
普通类:可以创建多个对象,
new A()每次都是不同的对象 -
单例类:只能创建一个对象,无论调用多少次获取方法,返回的都是同一个对象
适用场景:
-
日志系统(整个程序只有一个日志输出对象)
-
数据库连接池(全局共享一组连接)
-
配置管理器(读取一次配置,全局使用)
-
线程池(全局只有一个线程池)
cpp
// 普通类:可以创建多个对象
A a1, a2; // a1 和 a2 是不同的对象
// 单例类:只能创建一个对象
A& a1 = A::getInstance();
A& a2 = A::getInstance();
// a1 和 a2 指向同一个对象
单例模式确保一个类在整个程序中只有一个实例对象。
核心理解 :单例对象和普通类对象不同,它必须是静态的 ,因为只有静态才能保证全局唯一。有两种方式可以创建这个唯一的对象:饿汉模式 和懒汉模式。
二、如何禁止外部创建对象?
要实现单例模式,首先需要禁止外部直接创建对象。
1. 构造函数私有化
cpp
class MyObject {
private:
int value;
// 构造函数私有化,外部无法创建对象
MyObject(int x = 0) : value(x) {
cout << "Create MyObject:" << this << endl;
}
};
说明 :构造函数私有化后,外部代码 MyObject obj(10); 会编译报错,无法直接创建对象。这是单例模式的第一步。
2. 禁止拷贝(C++11 方式)
cpp
class MyObject {
private:
MyObject(const MyObject&) = delete; // 删除拷贝构造函数
MyObject operator=(const MyObject&) = delete; // 删除赋值运算符
};
为什么需要禁止拷贝?
即使构造函数是私有的,仍然可以通过拷贝构造创建新对象:
cpp
MyObject& obj1 = MyObject::getInstance(); // 获取单例
MyObject obj2 = obj1; // 拷贝构造会创建新对象,违反单例原则
说明 :使用 = delete 可以让编译器禁止拷贝,从根源上杜绝。
3. 析构函数必须是公有的
cpp
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
说明:析构函数必须是公有的,否则外部无法销毁对象。单例对象通常在程序结束时自动销毁,析构函数需要能被外部调用。
三、饿汉模式
饿汉模式在程序启动时就创建好单例对象,无论是否使用。
1. 实现方式
cpp
class MyObject {
private:
int value;
static MyObject objx; // 静态对象
MyObject(int x = 0) : value(x) {
cout << "Create MyObject:" << this << endl;
}
MyObject(const MyObject&) = delete;
MyObject operator=(const MyObject&) = delete;
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
static MyObject& getObject() {
return objx; // 直接返回静态对象
}
};
// 静态成员在类外初始化
MyObject MyObject::objx(10);
说明:
-
static MyObject objx;在类内声明静态对象 -
MyObject MyObject::objx(10);在类外定义并初始化,调用私有构造函数,传入参数10 -
这个
objx就是整个程序中唯一的MyObject对象,其value被初始化为10 -
无论通过
getObject()获取多少次,返回的都是同一个对象
cpp
MyObject& obj1 = MyObject::getObject();
MyObject& obj2 = MyObject::getObject();
// obj1 和 obj2 指向同一个对象,value 都是 10
为什么类外可以调用私有构造函数?
MyObject::objx 这个写法表示 objx 是 MyObject 类的成员。既然是类的成员,就可以访问类的私有成员 ,包括私有构造函数。类的作用域 MyObject:: 内的代码,可以访问该类的私有成员。这不是"外部调用",而是"类内部定义的延续"。
外部代码直接 MyObject obj(10); 才是真正的外部调用,会报错。
注意:饿汉模式下,初始化参数是固定的。如果需要用不同参数初始化,就要用懒汉模式。
2. 使用示例
cpp
int main()
{
MyObject& obj1 = MyObject::getObject();
MyObject& obj2 = MyObject::getObject();
cout << "obj1地址:" << &obj1 << endl;
cout << "obj2地址:" << &obj2 << endl;
obj1.Print();
obj2.Print();
return 0;
}
说明 :obj1 和 obj2 指向同一个地址,说明获取的是同一个对象。无论调用多少次 getObject(),返回的都是唯一的单例对象。
四、懒汉模式(基础版)
懒汉模式在第一次调用时才创建单例对象,按需创建,节省资源。
1. 实现方式
cpp
class MyObject {
private:
int value;
static MyObject* pobj; // 静态指针,初始为 nullptr
MyObject(int x = 0) : value(x) { // 私有构造函数
cout << "Create MyObject:" << this << endl;
}
MyObject(const MyObject&) = delete; // 禁止拷贝构造
MyObject operator=(const MyObject&) = delete; // 禁止赋值
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
static MyObject& getObject(int x) {
if (pobj == nullptr) { // 第一次调用时,指针为空
pobj = new MyObject(x); // 创建对象
}
return *pobj; // 返回对象(第一次创建,后续直接返回)
}
};
MyObject* MyObject::pobj = nullptr; // 静态成员类外初始化
说明:
-
static MyObject* pobj是指针,初始为nullptr -
第一次调用
getObject()时,pobj == nullptr,创建对象 -
后续调用时,
pobj已不为空,直接返回已有对象
2. 线程安全问题
基础版懒汉模式在单线程 环境下没问题,但在多线程环境下存在安全隐患:
cpp
// 线程 A 和线程 B 同时调用 getObject
if (pobj == nullptr) { // 两个线程都可能看到 pobj == nullptr
pobj = new MyObject(x); // 可能创建两个对象!
}
问题 :两个线程可能同时发现 pobj == nullptr,然后各自创建一个对象,违反单例原则。
结果 :内存中可能存在多个 MyObject 对象,不再是单例。
五、懒汉模式(加锁版)
为了解决线程安全问题,加互斥锁保证同一时间只有一个线程能进入临界区。
cpp
#include <mutex>
class MyObject {
private:
int value;
static MyObject* pobj; // 静态指针,初始为 nullptr
static std::mutex m_mutex; // 互斥锁,保证线程安全
MyObject(int x = 0) : value(x) { // 私有构造函数
cout << "Create MyObject:" << this << endl;
}
MyObject(const MyObject&) = delete;
MyObject operator=(const MyObject&) = delete;
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
static MyObject& getObject(int x) {
m_mutex.lock(); // 加锁:同一时间只有一个线程能进入
if (pobj == nullptr) {
pobj = new MyObject(x); // 第一次调用时创建对象
}
m_mutex.unlock(); // 解锁:其他线程可以进入
return *pobj;
}
};
MyObject* MyObject::pobj = nullptr; // 静态成员类外初始化
std::mutex MyObject::m_mutex; // 互斥锁类外初始化
问题 :每次调用 getObject() 都会加锁和解锁,即使对象已经创建(pobj != nullptr)也不例外。加锁操作本身有开销,在高并发场景下会影响性能。
示意图:
| 调用次数 | 对象状态 | 是否加锁 | 开销 |
|---|---|---|---|
| 第1次 | 未创建 | 是 | 创建对象 + 锁开销 |
| 第2次 | 已创建 | 是 | 锁开销(多余) |
| 第3次 | 已创建 | 是 | 锁开销(多余) |
结论:对象创建后,锁就没有必要了,但加锁版每次都要加锁,浪费性能。
六、双重检查锁定(DCL)
为什么叫"双重检查"?
双重检查指的是两次 if (pobj == nullptr) 判断,不是两个锁。
cpp
static MyObject& getObject(int x) {
if (pobj == nullptr) { // 第一次检查(无锁)
m_mutex.lock();
if (pobj == nullptr) { // 第二次检查(有锁)
pobj = new MyObject(x);
}
m_mutex.unlock();
}
return *pobj;
}
为什么需要两次检查?
问题:两个线程同时调用,对象还未创建。
第一次检查 :两个线程都看到 pobj == nullptr,都准备进入。
如果没有第二次检查:
cpp
// 线程A 和 线程B 同时执行
if (pobj == nullptr) { // 两个线程都看到 pobj 是空,都进入
m_mutex.lock();
pobj = new MyObject(x); // 线程A 创建对象1
m_mutex.unlock();
// 线程B 被锁挡住,等线程A 释放锁后,线程B 拿到锁
// 线程B 会继续执行 pobj = new MyObject(x)
// 线程B 创建对象2
}
结果 :创建了 两个对象 ❌
原因 :两个线程都通过了第一次检查,虽然加锁保证了同一时间只有一个线程执行创建,但解锁后另一个线程会再次创建对象,最终产生两个对象。
有第二次检查:
cpp
// 线程A 和 线程B 同时执行
if (pobj == nullptr) { // 两个线程都看到 pobj 是空,都进入
m_mutex.lock();
if (pobj == nullptr) { // 线程A:空,创建对象1
pobj = new MyObject(x);
}
m_mutex.unlock();
// 线程B 拿到锁后,先判断 pobj 是否为空
// 此时 pobj 已有对象1,所以跳过创建
}
结果 :只创建了 一个对象 ✅
第一次检查的作用
对象创建后,后续调用直接跳过加锁代码,性能好。
cpp
// 第2次、第100次调用时
if (pobj == nullptr) { // pobj 已不为空,直接跳过
// 加锁代码不会执行
}
return *pobj;
与加锁版对比
| 版本 | 每次调用都加锁? | 第100次调用 |
|---|---|---|
| 加锁版 | 是 | 加锁(多余) |
| DCL | 否 | 只做一次 if 判断,不加锁 |
注意事项
C++11 之后,推荐使用静态局部变量实现懒汉模式,更简洁:
cpp
static MyObject& getObject(int x) {
static MyObject objx(x);
return objx;
}
七、推荐使用:C++11 静态局部变量懒汉模式
C++11 标准保证:静态局部变量的初始化是线程安全的。
cpp
class MyObject {
private:
int value;
MyObject(int x = 0) : value(x) {
cout << "Create MyObject:" << this << endl;
}
MyObject(const MyObject&) = delete;
MyObject operator=(const MyObject&) = delete;
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
static MyObject& getObject(int x) {
static MyObject objx(x); // 静态局部变量,第一次调用时初始化
return objx;
}
};
优点:
-
代码最简洁
-
线程安全(C++11 保证)
-
按需创建,节省资源
注意 :getObject(int x) 的参数只对第一次调用有效,后续调用不会重新初始化。
cpp
MyObject& obj1 = MyObject::getObject(10); // 第一次调用,value = 10
MyObject& obj2 = MyObject::getObject(20); // 返回同一个对象,value 还是 10
推荐理由:这是目前实现单例模式最简洁、最安全的方式,优先使用。
八、多线程测试
下面的代码验证 C++11 静态局部变量懒汉模式在多线程环境下的线程安全性。
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class MyObject {
private:
int value;
MyObject(int x = 0) : value(x) {
cout << "Create MyObject:" << this << endl;
}
MyObject(const MyObject&) = delete;
MyObject operator=(const MyObject&) = delete;
public:
~MyObject() { cout << "Destroy MyObject:" << this << endl; }
void Print() const {
cout << "value:" << value << endl;
}
static MyObject& getObject(int x) {
static MyObject objx(x); // 静态局部变量,线程安全
return objx;
}
};
void threadfunc1() {
MyObject& obj1 = MyObject::getObject(10);
obj1.Print();
}
void threadfunc2() {
MyObject& obj2 = MyObject::getObject(20);
obj2.Print();
}
int main() {
std::thread tha(threadfunc1);
std::thread thb(threadfunc2);
tha.join();
thb.join();
return 0;
}
输出(只会创建一个对象):
cpp
Create MyObject:0x...
value:10
value:10
说明:虽然两个线程传入了不同的参数(10 和 20),但只创建了一个对象,值为第一次调用时的 10。这证明:
-
静态局部变量只初始化一次
-
C++11 保证初始化过程是线程安全的
九、两种模式对比
| 对比项 | 饿汉模式 | 懒汉模式 |
|---|---|---|
| 创建时机 | 程序启动时 | 第一次调用时 |
| 线程安全 | 天然安全 | 需要处理 |
| 资源利用 | 可能浪费(不用也创建) | 按需创建,节省资源 |
| 启动速度 | 较慢 | 较快 |
| 实现复杂度 | 简单 | 较复杂 |
| 推荐场景 | 对象一定会用到、简单程序 | 对象不一定用到、多线程程序、C++11推荐 |
十、总结
| 知识点 | 核心要点 |
|---|---|
| 单例模式 | 确保一个类只有一个实例,提供全局访问点 |
| 禁止外部创建 | 构造函数私有化 + 删除拷贝构造(= delete) |
| 饿汉模式 | 程序启动时创建静态对象,线程安全,但可能浪费资源 |
| 懒汉模式(基础) | 第一次调用时创建,线程不安全 |
| 懒汉模式(加锁) | 加互斥锁保证安全,但每次调用都加锁,性能差 |
| 双重检查锁定(DCL) | 两次检查 + 一个锁,创建后不再加锁,兼顾安全和性能 |
| C++11 静态局部变量 | 代码最简洁,线程安全,按需创建,推荐使用 |
| 单例对象必须是静态的 | 只有静态才能保证全局唯一 |
| 类外可以调用私有构造函数 | 类外定义静态成员时仍在类作用域内,可以访问私有成员 |