单例模式笔记

整理一下单例模式的笔记。

概念

先明确一下单例模式的概念,单例模式是创建类型设计模式中的一种。通过单例模式创建的类在当前进程中只有一个实例。
说到这里,可能会想到使用static关键字创建的静态局部变量,这也算是一种单例模式的简单应用,但我们还是应该从面对对象的角度来进一步理解这种设计模式。

按照单例模式的概念,单例模式的要点有三个:

  1. 这个类只能有一个实例,且只能被创建一次。
  2. 这个类必须自己创建这个实例,无法通过其他类来创建。
  3. 这个类必须要向整个系统提供这个实例。

那么下面按照这些要求实现这个类,这里我选择使用 C++ 来描述单例模式,由于 C++11 与其之前语言标准存在一定程度上的差异,本文也会从两个角度进行讨论。

C++11 之前

由于单例模式又分为懒汉式和饿汉式,所以我们分别描述。

饿汉式

那么什么是饿汉式单例模式呢?其实就是在程序执行开始时,立即创建单例,那么按照前面提到的规则,我们可以写出下面的代码:

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
#include <iostream>

class Singleton {
public:
static Singleton* getInstance() {
return m_singleton;
}
static void destoryInstance() {
if(m_singleton != NULL) {
delete m_singleton;
m_singleton = NULL;
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
};

Singleton* Singleton::m_singleton = new Singleton;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

上述代码中,注意一下几个要点:

  1. 为了保证单例的唯一性,需要私有化构造函数、析构函数、拷贝构造函数和拷贝赋值运算符来禁止在类外其他地方创建单例。
  2. 使用静态成员变量的方式来创建单例对象,有人可能会疑惑为什么可以在类外调用私有构造函数。实际上,C++ 要求静态成员变量在类外初始化,但由于我们加上了Singleton::,所以后面对私有构造函数的调用依然是在类的作用域内进行的(同时也是由类的内部成员调用)。
  3. 提供了唯一全局接口getInstance()访问单例,同时由于析构函数私有,为了防止内存写漏,又提供了destoryInstance()函数释放资源。
  4. 饿汉式单例的优点是线程安全(因为使用之前就已被创建且只创建一个,后续访问无需改动),但会带来一定程度上的性能开销。

懒汉式

懒汉式单例其实就是只有在使用时,才创建单例对象,这点从名字上也可以得到体现。有了前面饿汉式单例的铺垫,我们很容易写出下面的懒汉式单例代码:

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
#include <iostream>

class Singleton {
public:
static Singleton* getInstance() {
if(m_singleton == NULL)
m_singleton = new Singleton;
return m_singleton;
}
static void destoryInstance() {
if(m_singleton != NULL) {
delete m_singleton;
m_singleton = NULL;
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
};

// 不要忘记静态成员变量需要在类外定义
Singleton* Singleton::m_singleton = NULL;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

但是观察代码,会发现存在线程不安全的问题。那对应的解决方式是什么呢?显然可以通过加锁的方式解决,但 C++11 之前标准库是没有相关的类可以使用的,所以还得借助第三方库来完成,老式的库其实已经没有太大的探究意义了,这里就不继续讨论了。

C++11

饿汉式

有了前面的铺垫,我们很容易得到下面的代码:

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
#include <iostream>

class Singleton {
public:
static Singleton* getInstance() {
return m_singleton;
}
static void destoryInstance() {
if(m_singleton != nullptr) {
delete m_singleton;
m_singleton = nullptr;
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
};

Singleton* Singleton::m_singleton = new Singleton;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

上述代码中,我们使用 C++11 的新特性,直接显式的删除拷贝构造函数和拷贝赋值运算符。同时,我们依然保留了单例对象的创建和删除,这也是线程安全的。
实际上,在 C++11 新特性中,规定了静态局部变量的初始化是线程安全的。编译器会为静态局部变量的初始化加上锁,确保只有一个线程可以进行初始化,其他线程会等待初始化完成后再使用。根据这个特性,我们可以继续简化代码:

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
#include <iostream>

class Singleton {
public:
static Singleton* getInstance() {
static Singleton singleton;
return &singleton;
}

void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
};


int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;
return 0;
}

在上述代码中,我们使用静态局部变量替换掉了静态成员变量,这样做的好处是显而易见的:

  1. 依然是线程安全的,不需要考虑其他线程的情况。
  2. 静态局部变量会在离开作用域后自动析构掉(代码中是程序执行结束时自动析构),不需要在考虑何时析构的问题。

懒汉式

接着,我们再看看懒汉式,先简化一下之前的代码:

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
#include <iostream>

class Singleton {
public:
static Singleton* getInstance() {
if(m_singleton == nullptr)
m_singleton = new Singleton;
return m_singleton;
}
static void destoryInstance() {
if(m_singleton != nullptr) {
delete m_singleton;
m_singleton = nullptr;
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
};

// 不要忘记静态成员变量需要在类外定义
Singleton* Singleton::m_singleton = nullptr;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

在上述代码中,我们只是简单显式删除拷贝构造函数和拷贝赋值运算符,并没有解决前面提到的线程安全的问题。下面我们使用 C++11 提供的mutex来解决这个问题:

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
53
#include <iostream>
#include <mutex>

class Singleton {
public:
static Singleton* getInstance() {
m_mutex.lock();
if (m_singleton == nullptr)
m_singleton = new Singleton;
m_mutex.unlock();
return m_singleton;
}
static void destoryInstance() {
m_mutex.lock();
if (m_singleton != nullptr) {
delete m_singleton;
m_singleton = nullptr;
}
m_mutex.unlock();
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
static std::mutex m_mutex;
};

// 不要忘记静态成员变量需要在类外定义
Singleton* Singleton::m_singleton = nullptr;
std::mutex Singleton::m_mutex;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
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
53
54
55
56
57
#include <iostream>
#include <mutex>

class Singleton {
public:
static Singleton* getInstance() {
if (m_singleton == nullptr) {
m_mutex.lock();
if (m_singleton == nullptr)
m_singleton = new Singleton;
m_mutex.unlock();
}
return m_singleton;
}
static void destoryInstance() {
if (m_singleton != nullptr) {
m_mutex.lock();
if (m_singleton != nullptr) {
delete m_singleton;
m_singleton = nullptr;
}
m_mutex.unlock();
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static Singleton* m_singleton;
static std::mutex m_mutex;
};

// 不要忘记静态成员变量需要在类外定义
Singleton* Singleton::m_singleton = nullptr;
std::mutex Singleton::m_mutex;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

观察上述代码,可以发现我们所做的事情,其实就是在不同进程获取锁之前先判断单例对象是否已经创建,如果创建就直接返回。这样做之后,尽管第一次访问时仍然会造成阻塞,但后续的访问都不会造成阻塞。这样看起来好像皆大欢喜了,可惜的是双重检查锁定又会产生新的问题😓。
由于 C++ 编译器支持指令重排(优化指令顺序以提高性能),所以在执行new操作后,对应的对象并不是一定就已经构造好了,可能编译器只是分配了内存并返回了对应的地址而已,这是第一个进程解锁后,第二个进程就会直接返回整个地址了,可这时单例对象并没有构造好,又怎么访问呢?
所以,这里我们还需要想办法确保这块代码操作的原子性,好消息是别人已经想到这个问题了😂。
C++ 提供了atomic类来帮助我们解决这个问题:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <mutex>
#include <atomic>

class Singleton {
public:
static Singleton* getInstance() {
Singleton* singleton = m_singleton.load();
if (singleton == nullptr) {
m_mutex.lock();
singleton = m_singleton.load();
if (singleton == nullptr) {
singleton = new Singleton;
m_singleton.store(singleton);
}
m_mutex.unlock();
}
return singleton;
}
static void destoryInstance() {
Singleton* singleton = m_singleton.load();
if (singleton != nullptr) {
m_mutex.lock();
singleton = m_singleton.load();
if (singleton != nullptr) {
delete singleton;
singleton = nullptr;
m_singleton.store(singleton);
}
m_mutex.unlock();
}
}
void doSomething() {
std::cout << "do something..." << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {
std::cout << "singleton create." << std::endl;
};
~Singleton() {
std::cout << "singleton delete." << std::endl;
};
static std::mutex m_mutex;
static std::atomic<Singleton*> m_singleton;
};

// 不要忘记静态成员变量需要在类外定义
std::mutex Singleton::m_mutex;
std::atomic<Singleton*> Singleton::m_singleton;

int main() {
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();

s1->doSomething();
s2->doSomething();

std::cout << s1 << std::endl << s2 << std::endl;

s1->destoryInstance();
s2->destoryInstance();
return 0;
}

在上面的代码中,我们使用atomic类保存了一个Singleton*的指针,并创建了一个静态成员对象m_singleton,这个对象读取指针时会确保对应操作的原子性。换句话说,就是会保证使用load()函数读取的指针所指向的对象一定会被构建好。

总结

前面说了那么多,其实会发现最好且简单的方法就只有一种:使用静态局部对象,饿汉式创建单例。这种模式既简单,又不存在线程安全的问题,使用起来还很方便,唯一不足的就是会带来性能消耗。但对比整个工程而言,局部单例模式所消耗的性能估计不值一提,当然前提是使用的次数不多😂。
另外,单例模式通常用于需要控制对象资源的开发场景,一个类只创建一个对象的设计,既可以避免创建过多副本所造成的资源浪费现象,又可以避免引发数据一致性等问题。在数据库连接、线程池设计、日志系统设计等开发场景,经常使用单例模式来创建对象,可以有效地降低对内存资源的占用。
PS:之前一直不清楚构造函数私有化的应用场景,现在这个就是,看来还是自己懂得太少😓...

简单应用

下面我们按照饿汉式单例模式的思路,实现一个简单的消息队列。
思路也比较简单,我们直接套用上面的代码即可。但我们需要思考,如何存储消息,存储什么样的消息。这里,为了简单起见,就用普通的队列作为消息的容器,而消息由整数代替即可,那么我们可以使用queue<int>来存储消息。对应的,我们需要提供一系列的函数供生产者消费者调用,比如生产者向消息队列中放入消息,消费者从消息队列中取出消息。
按照上面的思路,我们可以得到下面的代码:

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
53
54
55
56
57
58
59
#include <iostream>
#include <queue>

class messageQueue {
public:
static messageQueue* getInstance() {
static messageQueue mq;
return &mq;
}

bool isEmpty() {
bool flag = m_q.empty();
return flag;
}

void pushMsg(int msg) {
m_q.push(msg);
}

bool popMsg() {
if(m_q.empty())
return false;
m_q.pop();
}

int topMsg() {
if(m_q.empty()) {
return -1;
}
int msg = m_q.front();
return msg;
}

messageQueue(const messageQueue&) = delete;
messageQueue& operator=(const messageQueue&) = delete;
private:
messageQueue() {
std::cout << "message queue created. " << std::endl;
};
~messageQueue() {
std::cout << "message queue deleted. " << std::endl;
};
std::queue<int> m_q;
};

int main() {
messageQueue* mq1 = messageQueue::getInstance();
messageQueue* mq2 = messageQueue::getInstance();

std::cout << mq1 << std::endl << mq2 << std::endl;
return 0;
}
/*
output:
message queue created.
0x7ff6474b40e0
0x7ff6474b40e0
message queue deleted.
*/

根据执行代码后的结果,这个消息队列的单例模式就被我们创建好了,访问单例对象也是也没有问题的。前面我们提到了生产者和消费者会对消息队列内存储的消息进行操作,若假设由不同的进程分别扮演生产者和消费者的角色,那么势必也会产生资源抢占的情况。也就是说,不同进程通过我们提供的pushMsg()等函数,操作消息队列内部数据时,还需要加锁来保证资源不会产生抢占。那么,我们可以进一步优化我们的代码:

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
53
54
55
56
#include <iostream>
#include <queue>
#include <mutex>

class messageQueue {
public:
static messageQueue* getInstance() {
static messageQueue mq;
return &mq;
}

bool isEmpty() {
std::lock_guard<std::mutex> locker(m_m);
bool flag = m_q.empty();
return flag;
}

void pushMsg(int msg) {
std::lock_guard<std::mutex> locker(m_m);
m_q.push(msg);
}

bool popMsg() {
std::lock_guard<std::mutex> locker(m_m);
if(m_q.empty())
return false;
m_q.pop();
}

int topMsg() {
std::lock_guard<std::mutex> locker(m_m);
if(m_q.empty()) {
return -1;
}
int msg = m_q.front();
return msg;
}

messageQueue(const messageQueue&) = delete;
messageQueue& operator=(const messageQueue&) = delete;
private:
messageQueue() {
std::cout << "message queue created. " << std::endl;
};
~messageQueue() {
std::cout << "message queue deleted. " << std::endl;
};
std::queue<int> m_q;
std::mutex m_m;
};

int main() {
messageQueue* mq1 = messageQueue::getInstance();

return 0;
}

在上面的代码中,我们通过模板类lock_guard来完成加锁的操作,同时,在这个作用域结束后,开头使用该类声明的对象locker就会被自动回收掉,从而完成解锁的操作。
下面我们在使用多线程来测试一下代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>

class messageQueue {
public:
static messageQueue* getInstance() {
static messageQueue mq;
return &mq;
}

bool isEmpty() {
std::lock_guard<std::mutex> locker(m_m);
bool flag = m_q.empty();
return flag;
}

void pushMsg(int msg) {
std::lock_guard<std::mutex> locker(m_m);
m_q.push(msg);
}

bool popMsg() {
std::lock_guard<std::mutex> locker(m_m);
if(m_q.empty())
return true;
m_q.pop();
return false;
}

int topMsg() {
std::lock_guard<std::mutex> locker(m_m);
if(m_q.empty()) {
return -1;
}
int msg = m_q.front();
return msg;
}

messageQueue(const messageQueue&) = delete;
messageQueue& operator=(const messageQueue&) = delete;
private:
messageQueue() {
std::cout << "message queue created. " << std::endl;
};
~messageQueue() {
std::cout << "message queue deleted. " << std::endl;
};
std::queue<int> m_q;
std::mutex m_m;
};

int main() {
messageQueue* mq = messageQueue::getInstance();

std::thread producer([=] {
for(int i = 0; i < 10; ++i) {
mq->pushMsg(i);
std::cout << "+++producer pushMsg: " << i << ", threadID: "
<< std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
});

std::thread consumer([=]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
while(!mq->isEmpty()) {
int msg = mq->topMsg();
mq->popMsg();
std::cout << "---consumer getMsg: " << msg << ", threadID: "
<< std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
});

producer.join();
consumer.join();

return 0;
}

/*
output:
message queue created.
+++producer pushMsg: 0, threadID: 2
---consumer getMsg: 0, threadID: 3
+++producer pushMsg: 1, threadID: 2
+++producer pushMsg: 2, threadID: 2
---consumer getMsg: 1, threadID: 3
+++producer pushMsg: 3, threadID: 2
+++producer pushMsg: 4, threadID: 2
---consumer getMsg: 2, threadID: 3
+++producer pushMsg: 5, threadID: 2
+++producer pushMsg: 6, threadID: 2
---consumer getMsg: 3, threadID: 3
+++producer pushMsg: 7, threadID: 2
+++producer pushMsg: 8, threadID: 2
---consumer getMsg: 4, threadID: 3
+++producer pushMsg: 9, threadID: 2
---consumer getMsg: 5, threadID: 3
---consumer getMsg: 6, threadID: 3
---consumer getMsg: 7, threadID: 3
---consumer getMsg: 8, threadID: 3
---consumer getMsg: 9, threadID: 3
message queue deleted.
*/

在上述代码中,我们使用了producerconsumer两个线程来测试代码,其中producer作为生产者,向消息队列中添加消息,consumer作为消费者,从消息队列中获取消息。观察最后的输出,可以发现,消费者获取消息的顺序与生产者添加消息的顺序符合队列的特征。同时,由于我们故意设置消费者在每次获取消息后休眠 1 秒,所以最后输出了一系列连续的消费信息,这些也是符合预期的。


Buy me a coffee ? :)
0%