介绍
单例模式的目的是确保某个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取该实例。这个模式常常用来管理全局共享资源,如配置对象、数据库连接、线程池等。
如下所示,我们通过new运算符创建出了类A的三个对象实例,而我们现在要做的是,如何设计类A,使得上述代码运行之后永远只产生同一个对象实例
1
2
3
|
A* p1=new A();
A* p2=new A();
A* p3=new A();
|
单例模式的核心要点:
- 唯一性:保证类只有一个实例。
- 全局访问点:提供一个全局的静态方法来访问该实例。
单例模式的结构:
- 私有构造函数:防止外部通过
new
关键字直接实例化对象。
- 静态成员变量:用于存储唯一的实例。
- 静态方法:提供外部获取实例的全局访问点。
设计方法
接下来我们一步步设计代码,来观察单例模式是如何被设计出来的
将构造函数设为私有
这一步的目的在于拒绝用户使用构造函数创建对象,只能使用我们提供的静态方法创建一个全局对象
1
2
3
4
5
|
class Singleton{
public:
private:
Singleton(){};
};
|
声明全局对象并提供访问接口
我们知道静态成员变量的特点是它在类的所有实例之间是共享的,因此,我们可将类实例直接创建出来,并声明为static
1
2
3
4
5
6
|
class Singleton{
public:
private:
static Singleton instance_;
Singleton(){};
};
|
由于上述定义的实例对象是private的,因此我们需要定义一个公共接口,来供外部用户进行访问
1
2
3
4
5
6
7
8
9
|
class Singleton{
public:
static Singleton& getInstance(){
return instance_;
}
private:
static Singleton instance_;
Singleton(){};
};
|
该接口声明是static的原因是,当用户调用接口时,对象实例还没有被创建出来,假如声明为普通成员函数,则普通成员函数的调用必须通过对象进行,这将成为一个矛盾点,因此如果想要用户使用这个函数,就必须要将其声明为static的
拒绝对象的拷贝和赋值
至此,我们已经完成了一个半成品,也就是说,这个时候我们要使用Sington对象,就必须通过其中的static成员函数getInstance来获取,而该函数返回的对象实例永远都是同一个实例
但还有个问题是,编译器还会为我们创建拷贝构造函数和拷贝赋值运算符,因此,当我们进行对象拷贝操作的时候,这个实例对象就不是“单例”的了
故而我们需要将拷贝构造和拷贝赋值运算符禁止掉
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Singleton{
public:
static Singleton& getInstance(){
return instance_;
}
private:
static Singleton instance_;
Singleton(){};
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
|
单例模式的分类
饿汉式单例模式
饿汉式单例模式是指,当在类加载时就将实例创建出来了,上述我们实现的单例模式就是饿汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#if 1
class Singleton{
public:
static Singleton& GetInstance(){
return instance_;
}
private:
static Singleton instance_;
Singleton(){}
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
//初始化静态变量
Singleton Singleton::instance_;
#endif
|
饿汉式单例模式的特点是
-
优点:
- 实现简单、线程安全:在类加载时就创建了实例,不存在多线程并发访问的问题
- 执行效率高、没有加锁同步等额外操作
-
缺点:
- 内存浪费:类加载时即创建实例,如果单例对象占用大量资源或者初始化耗时较长,会导致程序启动变慢。
- 无法延迟加载:即使没有使用到该单例对象,也会被创建
懒汉式单例模式
由于饿汉式单例模式在类加载时就会创建对象,即使我们不使用它它也会存在,当对象数据占据空间较大时,这显然是一种浪费,因此我们设计出了第二种单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Singleton
{
public:
static Singleton* GetInstance(){
// 将对象实例的创建延迟到第一次访问时
if(instance_==nullptr){
instance_=new Singleton();
}
return instance_;
}
private:
static Singleton *instance_;
Singleton(){};
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
Singleton* Singleton::instance_=nullptr;
|
如上,我们将类中创建的对象实例声明为指针类型,这样当类加载的时候,内存中存在的是一个指针,而指针变量的大小在相同的操作系统下永远都是固定的,除此以外,在GetInstance方法的实现中,我们还将对象实例的创建延迟到了第一次访问的时候,平时如果我们不是该对象,该对象就不会被new出来
懒汉式单例模式的线程安全问题
问题复现
在懒汉式单例模式的代码里,虽然可以实现对象的延迟加载,但是会存在多线程并发时代码的线程安全问题
考虑一种情况,假如现在有两个线程,都要执行如下代码创建单例对象的实例
1
|
Singleton* ptr1=Singleton::GetInstance();
|
那么
- 当线程1执行到这段代码时,开始执行下面这句代码时,突然切换回了线程2
- 而由于此时线程1并没有将对象创建创建出来就切换到了线程2,因此线程2也能执行到这段代码
那么问题来了,也就是说,此时线程1和线程2都执行到了上述这段代码,那么接下来,线程1和线程2就都可以创建这个“单例对象”
因此,在这种情况下,下述对象的创建其实是被执行了两次
1
|
instance_=new Singleton();
|
这就是懒汉式单例模式的线程安全问题
问题解决
方法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
|
class Singleton{
public:
static Singleton* getInstance(){
std::lock_guard<std::mutex> lock(_mutex);
if(!instance){
instance_=new Singleton();
}
return instance_;
}
void show(){
std::cout<<"this is singleton!"<<std::endl;
}
private:
static Singleton* instance_;
static std::mutex _mutex;
Singleton(){};
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
Singleton* Singleton::instance_=nullptr;
std::mutex Singleton::_mutex;
|
但是这样写的效率就会及其底下,在GetInstance成员函数中,就为了解决一个初始化该类对象时的互斥问题,居然在GetInstance中增加互斥量,导致所有调用该函数的调用者线程都被互斥一下,这非常影响性能。
因为除了初始化那个时刻,其他的时候完全不需要互斥。一旦初始化完毕,不管是否互斥调用GetInstance,这个if(!instance)条件都不会成立,从而完全可以确保初始化完毕之后,“instance=new Singleton();”代码行绝不会被再次执行。
方法2
方法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
|
#include<mutex>
#if 1
class Singleton {
public:
static Singleton *GetInstance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_)
instance_ = new Singleton();
}
return instance_;
}
private:
static Singleton *instance_;
static std::mutex mtx_;
Singleton() {};
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
};
// 静态成员变量定义:初始化为 nullptr
Singleton* Singleton::instance_ = nullptr;
// 静态成员变量初始化:mutex
std::mutex Singleton::mtx_;
#endif
|
使用互斥锁可以避免竞态问题的出现,但是会代码效率问题,但是如果我们此时在外层多加一层判断,那么一旦instance被创建成功后,因为最外面有一个条件判断if (instance ==NULL)在,这样就不会每次调用GetInstance都会创建一次互斥量。
也就是说,平常的调用根本就执行不到创建互斥量的代码,而是直接执行“return instance_;”,这样调用者就能够直接拿到这个单例类的对象,所以肯定提高了执行GetInstance的效率。
因此,互斥锁解决了多线程对单例对象创建的竞态问题,而双重判断解决了互斥锁带来的效率问题