八股篇|重新被考察的单例模式

单例模式作为一种设计模式,是每个程序员的必修模式。作为以前的八股代表——更多的是在面试的时候询问懒汉模式饿汉模式的区别。但当前随着整体互联网大环境不好,手撕单例模式成为了一个常态的面试问题,可以更加灵活的考察。

什么是单例模式?

单例模式是创建型设计模式,为了确保类在程序运行过程中只有一个实例,并只提供一个全局访问点,如: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
class LazySingleton {
private:
// 声明静态实例指针
static LazySingleton* instance;
// 私有构造函数,防止外部直接实例化
LazySingleton() {
std::cout << "LazySingleton instance created." << std::endl;
}

public:
// 静态方法,获取单例实例
static LazySingleton* getInstance() {
// 如果实例不存在,则创建一个新实例
if (instance == nullptr) {
instance = new LazySingleton();
}
return instance;
}

// 其他成员方法
void showMessage() {
std::cout << "Hello from LazySingleton! instance = "<< &instance << std::endl;
}
};

// 初始化静态实例指针为nullptr
LazySingleton* LazySingleton::instance = nullptr;

饿汉模式

  • 实例预先创建:在类加载的时候就创建实例,不管是否使用,实例都会被创建
  • 线程安全:类在加载时就被创建,因此不存在多线程情况下竞争,是线程安全的。
  • 实现方式:简单,但是长时间不使用浪费资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 饿汉式单例
class EagerSingleton {
private:
static EagerSingleton* instance;
EagerSingleton() {
std::cout << "EagerSingleton instance created." << std::endl;
}

public:
static EagerSingleton* getInstance() {
return instance;
}

// 其他成员方法
void showMessage() {
std::cout << "Hello from EagerSingleton! instance = " << &instance << std::endl;
}
};

EagerSingleton* EagerSingleton::instance = new EagerSingleton();

简单测试一下生成结果

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
int main() {
// 获取单例实例
LazySingleton* singleton1 = LazySingleton::getInstance();
singleton1->showMessage();

// 尝试再次获取实例,应该得到相同的实例
LazySingleton* singleton2 = LazySingleton::getInstance();
singleton2->showMessage();

if (singleton1 == singleton2)
std::cout << "1,2两个实例是同一个对象" << std::endl;

// 获取单例实例
EagerSingleton* singleton3 = EagerSingleton::getInstance();
singleton3->showMessage();

// 尝试再次获取实例,应该得到相同的实例
EagerSingleton* singleton4 = EagerSingleton::getInstance();
singleton4->showMessage();

if (singleton3 == singleton4)
std::cout << "3,4两个实例是同一个对象" << std::endl;

// 注意:不要尝试通过构造函数直接创建实例,因为构造函数是私有的
// EagerSingleton* invalidInstance = new EagerSingleton(); // 错误,无法访问私有构造函数

return 0;
}

//输出结果为
EagerSingleton instance created.
LazySingleton instance created.
Hello from LazySingleton! instance = 0024E3D0
Hello from LazySingleton! instance = 0024E3D0
1,2两个实例是同一个对象
Hello from EagerSingleton! instance = 0024E3D4
Hello from EagerSingleton! instance = 0024E3D4
3,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
class Singleton {
Singleton() = default;
~Singleton() = default;

// Delete the copy and move constructors
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton* pInstance;
static std::mutex lock;

public:
static Singleton* get() {
std::lock_guard<std::mutex> lc(lock);
if (pInstance == nullptr) {
pInstance = new Singleton;
}
return pInstance;
}
};

Singleton* Singleton::pInstance = nullptr;
std::mutex Singleton::lock;

上述单例模式可以正常使用,但是为了更好的解决线程不安全这一情况——双重检查锁模式(Double-Checked Locking Pattern,DCLP)成为了更好的方法。

有兴趣的可以查看这篇文章——DCLP的解释

上述代码被改写为

1
2
3
4
5
6
7
8
9
static Singleton* get() {
if (pInstance == nullptr) {
std::lock_guard<std::mutex> lc(lock);
if (pInstance == nullptr) {
pInstance = new Singleton;
}
}
return pInstance;
}

虽然安全性已经大大提升,但是这里还是有一个隐形的缺陷,异常情况下编译器的调用顺序。

一个在stackoverflow的调用问题

程序调用顺序异常情况下调转

调用顺序的错误,导致DCLP机制也无法真正的做到完全安全。因为new实例的过程是不确定的。所以现代C++11有了一个新的单例模式写法。

现代单例模式

Magic statics, more formally known asfunction-local static initialization, is when astatic variable is declared within a function, and then passed to the caller.

Since C++11, the initialization of magic statics is guaranteed to be thread safe. Internally, the first thread that calls GetString() will initialize the pointer, blocking until initialization is complete. All other threads will then use the initialized value. Before C++11, calling GetString() from multiple threads is classified as undefined behaviour (UB).

这里我直接说明一下,从C++11开始,static变量进行实例化过程时会保证线程的安全。第一个线程将初始化指针,阻塞直到初始化完成。然后,所有其他线程都将使用初始化后的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename T>
class tSingleton {
public:
static T& getInstance() {
static T instance; // 静态局部变量确保只初始化一次
return instance;
}

tSingleton(const tSingleton&) = delete;
tSingleton& operator=(const tSingleton&) = delete;

protected:
tSingleton() {} // 构造函数和析构函数设为protected,防止外部直接实例化和销毁
virtual ~tSingleton() {}
};

class MySingleton : public tSingleton<MySingleton> {
public:
void doSomething() {
std::cout << "Doing something in MySingleton" << std::endl;
}
};

个人常用的现代写法

我个人比较喜欢使用C++11的新特性,借助std::call_oncestd::once_flag。这俩是C++11新引入的新特性,用于支持一次性初始化和线程安全的调用。它们提供了一种简单而有效的方法来确保在多线程环境下某个函数只被调用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <mutex>

class Singleton {
private:
static std::once_flag oc; // 只执行一次的标记
static Singleton* instance;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
static Singleton* getInstance() {
std::call_once(oc, []{ instance = new Singleton(); });
return instance;
}
};

// 初始化静态成员变量
std::once_flag Singleton::oc;
Singleton* Singleton::instance = nullptr;