再谈单例

单例模式

饿汉式

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
#include <iostream>
#include <memory>
class Singleton{
public:
static Singleton* getInstance(){
return m_instance;
}

void printAddr(){
std::cout << m_instance << std::endl;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

private:
Singleton() = default;
~Singleton() = default;
private:
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = new Singleton(); // 提前创建,调用 getInstance 必然已经初始化成功

饿汉式本质上就是线程安全。饿汉单例模式在类加载时就创建实例,因为它是在类加载时就初始化的,所以即使在多线程环境中,它的实例创建也是线程安全的。

然而,饿汉单例有一个缺点,就是无论是否使用该单例,它都会在程序启动时就被创建,可能会导致资源浪费,特别是在该单例较为庞大或初始化较为复杂时。

 

为了和下面要讲的懒汉式进行区分,这里提前讲讲区别。饿汉会提前创建 static 对象,并在类外初始化,这就是为了在类加载时就创建实例,等到外部调用 getInstance 的时候必然已经把 单例对象创建好,也就必然是线程安全,得到的单例都是同一个单例对象,不线程安全才怪。

懒汉式

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
#include <iostream>
#include <memory>
class Singleton{
public:
static Singleton* getInstance(){
if (!m_instance){
m_instance = new Singleton(); // 需要的时候再创建,存在线程安全问题
}
return m_instance;
}

void printAddr(){
std::cout << m_instance << std::endl;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

private:
Singleton() = default;
~Singleton() = default;
private:
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;

懒汉式会在用户实际调用 getinstance 的时候才会创建 static 对象,所以原生的懒汉式存在线程安全问题。

懒汉传参问题

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
#include <iostream>
#include <memory>
class Singleton{
public:
static Singleton* getInstance(){
if (!m_instance){
m_instance = new Singleton(m_num);
}
return m_instance;
}

void printAddr(){
std::cout << m_instance << std::endl;
}

void init(int num){
m_num = num;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

private:
Singleton(int num){
std::cout <<"num = "<<num <<std::endl;
}
~Singleton() = default;
private:
static int m_num;
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;
int Singleton::m_num = 10;

创建一个共有的 init 函数,进而提前把传输传递到类中,等到创建对象的时候就可以传参初始化。

1
2
3
4
5
static int num = 100;
void Test(){
Singleton::getInstance()->init(num + 10);
Singleton::getInstance()->printAddr();
}

线程安全的单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <memory>
class Singleton{
public:
static Singleton& getInstance(){
static Singleton m_instance;
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

private:
Singleton() = default;
~Singleton() = default;
};

为什么它是线程安全的?

C++11及以后,静态局部变量的初始化是线程安全的。

单例汇编.png

采用了线程安全的方式(使用 __cxa_guard_acquire__cxa_guard_release)来确保单例实例的初始化过程只会发生一次,避免多线程并发时的初始化问题。

call_once 和 once_flag

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
#include <iostream>
#include <memory>
#include <mutex>
class Singleton{
public:
static Singleton& getInstance(){
static std::once_flag m_flag; // 必须得被实际修改,也就是说 once_flag 必须得是全局或静态
std::call_once(m_flag,[&](){
m_instance = new Singleton();
});
return *m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;

private:
Singleton() = default;
~Singleton() = default;
private:
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;

前面的那种 创建静态局部对象的方式不推荐,原因如下:

  • 在程序结束时,静态局部变量的析构函数会被调用。对于 静态局部变量(即局部静态变量 static Singleton instance),它们的析构时间通常是在 main 函数或程序结束时,而这一点在多线程环境中可能引发潜在的 析构顺序问题,尤其是在存在其他全局或静态对象的情况下。
  • 如果你的单例对象依赖于其他资源或全局对象,且这些资源在析构时被销毁,而 Singleton 被销毁时它们尚未销毁,就可能出现问题。例如,如果单例的析构过程中需要依赖其他全局对象时,可能会访问到已经销毁的资源。

因此,我们这里用的是堆内存来创建的对象。那这个堆对象如何进行回收?进入下一个主题!

单例对象自动释放

call_once 和 once_flag 结合 atexit(线程安全)

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
#include <iostream>
#include <memory>
#include <mutex>
class Singleton{
public:
static Singleton* getInstance(){
static std::once_flag m_flag;
std::call_once(m_flag,[&](){
m_instance = new Singleton();
atexit(&destroy);
});
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static void destroy(){
if (m_instance){
delete m_instance;
m_instance = nullptr;
}
}
private:
static Singleton* m_instance;
};
Singleton* Singleton::m_instance = nullptr;

我们在 call_once 中 调用 atexit 注册(保证只会被注册一次)自实现的 destroy 方法回收静态对象指向的堆内存,可以确保程序结束会调用。

静态回收.png

通用的单例模板类

我们到这里已经解决线程安全问题,回收单例资源(通过堆创建的对象)。但是,每次要给某个类赋予单例的能力未免太麻烦,直接实现一个模板单例类,派生类继承之后调用 getInstance 就可以得到单例了。

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
template<typename T>
class Singleton{
public:
static T* getInstance(){
static std::once_flag m_flag;
std::call_once(m_flag,[&](){
m_instance = new T();
atexit(&destroy);
});
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected: // 因为要继承,所以从 private 改为 protected
Singleton() = default;
~Singleton() = default;
static void destroy(){
if (m_instance){
delete m_instance;
m_instance = nullptr;
}
}
private:
static T* m_instance;
};
template<typename T>
T* Singleton<T>::m_instance = nullptr;

如何使用?

1
2
3
4
5
6
7
8
9
10
11
class Student : public Singleton<Student> {
friend class Singleton<Student>;
public:
private:
Student() {
cout << "Student" << endl;
}
~Student(){
cout << "~Student" << endl;
}
};

然后 Student 就可以调用 getInstance 获取单例对象了。

引入智能指针

利用智能指针把 atexit 优化掉。

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
#include <memory>
#include <mutex>
template<typename T>
class Singleton{
public:
static std::shared_ptr<T> getInstance(){
static std::once_flag m_flag;
std::call_once(m_flag,[&](){
m_instance = std::make_shared<T>();
});
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
Singleton() = default;
~Singleton() = default;
protected:
static std::shared_ptr<T> m_instance;
};
template<typename T>
std::shared_ptr<T> Singleton<T>::m_instance = nullptr;

如果使用 unique_ptr

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
#include <iostream>
#include <memory>
#include <mutex>
template<typename T>
class Singleton{
public:
static std::unique_ptr<T> getInstance(){
static std::once_flag m_flag;
std::call_once(m_flag,[&](){
m_instance = std::make_unique<T>();
});
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
Singleton() = default;
~Singleton() = default;
protected:
static std::unique_ptr<T> m_instance;
};
template<typename T>
std::unique_ptr<T> Singleton<T>::m_instance = nullptr;

unique智能指针本身就已经删除拷贝构造和拷贝赋值,当我们 return m_instance 的时候会触发拷贝构造,进而出现错误。

1
2
3
void Test(){
Student::getInstance(); // 报错
}

所以我们返回的时候,通过 move 返回就可以使用了:

1
2
3
4
5
6
7
8
static std::unique_ptr<T> getInstance()
{
static std::once_flag m_flag;
std::call_once( m_flag, [&]() {
m_instance = std::make_unique<T>();
} );
return(std::move( m_instance ) ); // move
}

但是,我们不会用 unique 来管理我们的单例类,因为通常我们需要把单例类对象交给多个人使用。单例是保证只生成一个对象,但是并非希望某时刻只能由一个类使用,因此共享智能指针管理是我们想要的。

除非你能保证这个单例类永远只在当前类使用,那就没问题,毕竟 unique_ptr 比 shared_ptr 更轻量。

智能指针 和 atexit 比较 ?

智能指针存在被误用导致的双重 delete 问题 或 free 问题,但是 atexit 没有,因为它注册的 destory 函数会检查。但是这个优点智能指针不是不可以做到,只需要把下面的方法注册为智能指针的删除器即可。

1
2
3
4
5
6
7
8
static void destroy()
{
if ( m_instance )
{
delete m_instance;
m_instance = nullptr;
}
}

再有就是我们智能指针管理会有多个指针对一个堆对象进行管理以及引用计数资源,但是采用 atexit 来管理单例资源就只需要一个指针,也没有额外的引用计数资源要考虑,空间利用上 atexit 也更合理。

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 <memory>
#include <mutex>
template<typename T>
class Singleton{
public:
static T* getInstance(){
static std::once_flag m_flag;
std::call_once(m_flag,[&](){
m_instance = new T();
atexit(&destroy);
});
return m_instance;
}

// 删除拷贝构造和拷贝赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 删除移动构造和移动赋值构造
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
Singleton() = default;
~Singleton() = default;
static void destroy(){
if (m_instance){
delete m_instance;
m_instance = nullptr;
}
}
private:
static T* m_instance;
};
template<typename T>
T* Singleton<T>::m_instance = nullptr;

产生的愚蠢想法

返回 m_instance 是调用的什么函数?

我的意思是已经把 拷贝构造/拷贝赋值/移动构造/移动赋值 都 delete 了,这里不是要触发拷贝构造?

但是这里弄混了,m_instance 是一个 std::shared_ptr<T>,而 std::shared_ptr 实现了智能指针的拷贝和移动语义。

具体来说:

  1. 拷贝构造和拷贝赋值std::shared_ptr 会通过引用计数的方式共享对象,而不是拷贝对象本身。当 shared_ptr 被拷贝时,拷贝的只是智能指针的控制块(包含引用计数等信息),不会触发原对象的拷贝构造。因此,return m_instance; 会将 m_instance 中保存的 std::shared_ptr<T> 返回,并不会拷贝实际的 T 对象,只是增加引用计数
  2. 移动构造和移动赋值:如果 m_instance 是通过移动构造或移动赋值创建的(例如在某些优化情况下,编译器可能会选择使用移动语义),它会转移对原对象的所有权,而不是拷贝。移动语义会转移 shared_ptr 的控制权,而不会拷贝管理的资源。

在你提供的代码中,std::shared_ptr<T> m_instance;getInstance 中返回时,m_instance 的引用计数被递增,但没有涉及到原始对象的构造、复制、赋值或销毁过程,智能指针只是通过引用计数机制确保资源在多个地方共享。

所以,return m_instance; 在这里不会触发任何的拷贝或移动构造/赋值,只有智能指针本身的管理机制(即引用计数)会发生变化。

 

再者,抛开智能指针,这里也不会发生拷贝构造,因为这是静态对象。