单例模式
单例模式指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有以下两种实现方式:
# 饿汉模式
饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。
饿汉式单例是在类加载时就创建实例。它的优点是不需要考虑多线程同步问题,因为在类加载时就完成了初始化,在多线程高并发环境下频繁使用,性能要求较高;缺点是无论是否使用都会占用内存。也就是没有线程安全问题。
做法:
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的 static 指针,并在程序入口之前完成单例对象的初始化。
- 提供一个全局访问点获取单例对象。
// 饿汉模式
// 优点:简单
// 缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
class HungrySingleton {
public:
// 返回引用,避免指针操作
static HungrySingleton& GetInstance() {
return instance;
}
void DoSomething() {
std::cout << "Doing something..." << std::endl;
}
private:
// 私有构造函数
HungrySingleton() {} = default;
~HungrySingleton() {}
// 禁止拷贝和赋值
HungrySingleton(const HungrySingleton&) = delete;
HungrySingleton& operator=(const HungrySingleton&) = delete;
// 静态成员变量,在 main 之前初始化
static HungrySingleton instance;
};
// 在全局作用域初始化(main 函数之前执行)
HungrySingleton HungrySingleton::instance;
int main() {
HungrySingleton::GetInstance().DoSomething();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 懒汉模式
如果单例对象构造十分耗时或者占用很多资源,比如加载插件,初始化网络连接啊,读取文件等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。
- 将构造函数设置为私有,并将拷贝构造函数和赋值运算符重载函数设置为私有或删除,防止外部创建或拷贝对象。
- 提供一个指向单例对象的 static 指针,并在程序入口之前先将其初始化为空。
- 提供一个全局访问点获取单例对象。
// 懒汉模式
// 优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
// 缺点:复杂
class LazySingleton {
public:
static LazySingleton* GetInstance() {
// 第一次检查(无锁)
if (instance == nullptr) {
// 加锁
std::lock_guard<std::mutex> lock(mutex);
// 第二次检查(有锁)
if (instance == nullptr) {
instance = new LazySingleton();
}
}
return instance;
}
void DoSomething() {
std::cout << "Doing something..." << std::endl;
}
// 手动释放
static void Destroy() {
std::lock_guard<std::mutex> lock(mutex);
delete instance;
instance = nullptr;
}
private:
LazySingleton() {} = default;
~LazySingleton() {}
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static LazySingleton* instance;
static std::mutex mutex;
};
// 静态成员初始化
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mutex;
int main() {
// 第一次调用,会创建对象
LazySingleton::GetInstance()->DoSomething();
// 第二次调用,直接返回已有对象
LazySingleton::GetInstance()->DoSomething();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
双重检查锁定(Double-Checked Locking)
双重检查锁定是一种用于懒汉式单例模式的优化技术,主要目的是在多线程环境下创建单例对象时,既保证线程安全性,又避免不必要的加锁操作,提高性能。
为什么需要双重检查锁定? 当我们在多线程环境中创建单例时,如果不进行适当的同步控制,可能会导致多个线程同时创建实例,从而产生多个对象,违背了单例模式的初衷。为了解决这个问题,我们通常会对获取实例的过程加锁,但是简单的加锁会带来性能问题,因为加锁和解锁是一个耗时操作,会影响性能。
双重检查锁定是如何优化的?
双重检查锁定通过在加锁前后分别检查实例是否为空,减少了不必要的加锁操作。具体步骤如下:
static Singleton* getInstance() {
// 第一次检查(不加锁)
if (instance == nullptr) {
// 加锁,防止多个线程同时创建实例
std::lock_guard<std::mutex> lock(mutex_);
// 第二次检查(加锁后)
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 第一次检查:在加锁前检查 instance 是否为空。如果已经有实例了,就直接返回,无需加锁。
- 加锁:如果 instance 为空,说明实例还没有被创建,进行加锁操作,确保只有一个线程能够进入创建实例的代码块。
- 第二次检查:加锁后,再次检查 instance 是否为空。这样做是因为在第一个线程加锁前,其他线程可能已经创建了实例。只有在 instance 仍然为空时,才会创建新实例。
自 C++11 起,推荐利用静态局部变量的特性实现线程安全的单例模式:
static Singleton& getInstance() {
static Singleton instance; // C++11保证局部静态变量的线程安全
return instance;
}
1
2
3
4
2
3
4
在这个实现中,getInstance() 方法中的静态局部变量 instance 只会在第一次调用时初始化,并且 C++11 保证其线程安全。
上次更新: 2026/04/14, 21:59:44