前言
本文中所有设计模式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;
}
指令重排在多线程下的危害如下:(线程B返回一个未初始化的instance对象)
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 | class single{ |