AkiraZheng's Time.

设计模式1:单例模式(C++)

Word count: 1.8kReading time: 7 min
2024/01/26

前言

本文中所有设计模式Github代码
其中本文单例模式相关代码在SingletonPattern.h文件中

一、单例模式的实现原理

1. 什么是单例模式

单例模式是指某一个类在整个程序运行期间(系统生命周期),只有一个实例存在,因此要求构造函数是私有的(private),同时在多线程场景下需要保证线程安全,即多个线程中不会创建多个实例对象。

2. 使用单例模式的优点

节省资源:在内存中只有一个实例,减少了内存开销,可以避免对资源的多重占用

方便控制:设置全局访问点,严格控制访问范围,防止对象的重复创建,保证对象的唯一性,保证对象的线程安全

3. 单例模式的应用场景

  • 要求生产唯一序列号
  • Web页面的计数器,不用每次刷新都在数据库中加一,使用单例可以在内存中缓存计数器值
  • 创建一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等

4. 单例模式的线程安全问题

一种最粗暴的线程安全方法就是在代码一运行就初始化创建实例

单例模式根据类中实例化对象的不同,可以分为饿汉式(线程不安全)和懒汉式(线程安全)

  • 饿汉式:在类加载时就创建对象实例,线程不安全,无法在多线程中保证单例
  • 懒汉式:在类加载时不创建对象实例,而是在第一次调用时创建对象实例,可以在公有静态成员函数中,在实例化单例对象前进行同步加锁,保证线程安全

二、单例模式的实现代码(C++)

单例模式在代码中的基本UML类图:

单例模式代码实现的注意事项:

  • 类设计中构造方法置于私有区域,以防止外部创建对象
  • 类中定义一个私有静态成员变量,用于指向单例对象
    • 类中定义一个公有静态成员函数,用于获取单例对象 static Singleton *getInstance()
    • 根据该对象实例化的不同,可以分为饿汉式懒汉式

2.1 饿汉式:在类加载时就创建对象实例,在代码一运行main之前就初始化创建实例

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
class SingletonHungry
{
private:
SingletonHungry(){}; // 构造函数私有化
static SingletonHungry *instance; // 静态成员变量

// 禁止拷贝构造和赋值操作
SingletonHungry(const SingletonHungry &) = delete;
SingletonHungry &operator=(const SingletonHungry &) = delete;

// 测试用
int test_num = 10;

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

// 测试用
void resetNum(int num)
{
test_num = num;
}

int getNum()
{
return test_num;
}
};

// 类外定义,main开始执行前,该对象就存在了,本身就是线程安全的,整个程序中只有一个实例
SingletonHungry *SingletonHungry::instance = new SingletonHungry();
void testSingletonHungry()
{
cout << "origin->getNum() = " << SingletonHungry::getInstance()->getNum() << endl;
SingletonHungry *p1 = SingletonHungry::getInstance();
SingletonHungry *p2 = SingletonHungry::getInstance();
p1->resetNum(20);
cout << "p1->getNum() = " << p1->getNum() << endl;
cout << "p2->getNum() = " << p2->getNum() << endl;
cout << "p1 = " << p1 << endl;
cout << "p2 = " << p2 << endl;
}

int main()
{
testSingletonHungry();
return 0;
}

2.2 懒汉式:在类加载时不创建对象实例,而是在第一次调用时创建对象实例

2.2.1 懒汉式的线程不安全形式

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
class SingletonLazy1
{
private:
SingletonLazy1(){}; // 构造函数私有化
static SingletonLazy1 *instance; // 在类加载时不创建对象实例

// 禁止拷贝构造和赋值操作
SingletonLazy1(const SingletonLazy1 &) = delete;
SingletonLazy1 &operator=(const SingletonLazy1 &) = delete;

public:
static SingletonLazy1 *getInstance()
{
if (instance == NULL)
instance = new SingletonLazy1(); // 第一次调用时创建对象实例
return instance;
}
};
SingletonLazy1 *SingletonLazy1::instance = NULL;
void testSingletonLazy1()
{
SingletonLazy1 *p1 = SingletonLazy1::getInstance();
SingletonLazy1 *p2 = SingletonLazy1::getInstance();
cout << "p1 = " << p1 << endl;
cout << "p2 = " << p2 << endl;
}

2.2.2 懒汉式的双重检查锁定(DCL,即 double-checked locking)

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
/*
* 在声明实例化对象时,使用volatile修饰保证线程安全,否则可能出现指令重排
* 也就是在一个线程内,执行instance = new SingletonLazy2_DCL();时,
编译器可能会先分配内存空间,再初始化对象,最后将singleton指向分配的内存空间,
* 这样在多线程环境下,其他线程可能会在singleton初始化对象之后,
指向分配的内存空间之前就访问了singleton,这样其他线程在执行if (instance == nullptr)时,会判断为false,但是此时singleton指向的对象还没指向内存空间,这样实际上其它线程返回的就是一个nuullptr指针,而不是一个实例化的对象
* 实现实例化 singleton = new Singleton() 可以分为3步
* 1.分配内存空间
* 2.初始化对象
* 3.将singleton指向分配的内存空间,避免指令重排
*/
class SingletonLazy2_DCL
{
private:
SingletonLazy2_DCL(){}; // 构造函数私有化
volatile static SingletonLazy2_DCL *instance; // 必须用volatile修饰
static mutex mtx;
public:
volatile static SingletonLazy2_DCL *getInstance() // 需要进行同步加锁,保证线程安全
{
if (instance == nullptr)
{ // 第一次检查
mtx.lock(); // 加锁
if (instance == nullptr)
{ // 第二次检查,防止多个线程同时进入临界区
instance = new SingletonLazy2_DCL(); // 创建实例
}
mtx.unlock(); // 解锁
}
return instance;
}
};
volatile SingletonLazy2_DCL *SingletonLazy2_DCL::instance = nullptr;
mutex SingletonLazy2_DCL::mtx;// 静态成员变量需要在类外初始化
void testSingletonLazy2_DCL()
{
volatile SingletonLazy2_DCL *p1 = SingletonLazy2_DCL::getInstance();
volatile SingletonLazy2_DCL *p2 = SingletonLazy2_DCL::getInstance();
cout << "p1 = " << p1 << endl;
cout << "p2 = " << p2 << endl;
}

2.2.3 懒汉式的std::call_once(C++11),保证某个函数在多线程环境中只被调用一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Singleton {
private:
static Singleton* instance;
static std::once_flag onceFlag;

// 私有化构造函数
Singleton() {}

// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
// 获取单例对象的函数
static Singleton* getInstance() {
std::call_once(onceFlag, []() {
instance = new Singleton();
});
return instance;
}
};

2.2.4 C++11之后的局部静态变量是线程安全的

为了优雅地解决懒汉模式的线程安全问题,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。(也就是将静态变量的声明放在函数内部,使其成为函数内的局部静态变量,而不是在类private或public中声明)

C++11之后,编译器要求保证局部静态变量的线程安全,所以这种方法不需要加锁也能由编译器自动保证线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class single{
private:
single(){}
~single(){}

public:
static single* getinstance();

};

single* single::getinstance(){
static single obj;//局部静态变量:线程安全的
return &obj;
}

三、Reference

  1. 【C++】C++ 单例模式总结(5种单例实现方法)
  2. C++设计模式之单例模式详解(懒汉模式、饿汉模式、双重锁)
CATALOG
  1. 前言
  2. 一、单例模式的实现原理
    1. 1. 什么是单例模式
    2. 2. 使用单例模式的优点
    3. 3. 单例模式的应用场景
    4. 4. 单例模式的线程安全问题
  3. 二、单例模式的实现代码(C++)
    1. 2.1 饿汉式:在类加载时就创建对象实例,在代码一运行main之前就初始化创建实例
    2. 2.2 懒汉式:在类加载时不创建对象实例,而是在第一次调用时创建对象实例
      1. 2.2.1 懒汉式的线程不安全形式
      2. 2.2.2 懒汉式的双重检查锁定(DCL,即 double-checked locking)
      3. 2.2.3 懒汉式的std::call_once(C++11),保证某个函数在多线程环境中只被调用一次
      4. 2.2.4 C++11之后的局部静态变量是线程安全的
  4. 三、Reference