文章目录
1. 题目来源
《剑指-Offer》第二版,P32,面试题2:实现Singleton模式
2. 题目说明
设计一个类,我们只能生成该类的一个实例
3. 题目解析
3.1 单例模式为什么常考?
只能生成一个实例的类是实现了Singleton
(单例)模式的类型。设计模式在面向对象程序设计中起着举足轻重的作用,在面试过程中很多公司都喜欢问一些与设计模式相关的问题。在常用的模式中,Singleton
是唯一 一个能够用短短几十行代码完整实现的模式。因此,写一个 Singleton
的类型是一个很常见的面试题。
在博主的[C++系列]
中也对单例模式进行了讲解、实现,可参考以下两篇博文:
[C++系列] 42. 饿汉模式剖析—单例模式
[C++系列] 43. 懒汉模式剖析—单例模式
3.2 不好的解法一:只使用与单线程环境
由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例。我们可以定义一个静态的实例,静态成员在程序运行之前完成初始化,并提供一个静态方法获取单例静态成员。下面定义类型Singleton1
就是基于这个思路的实现,四大实现要点如下:
- 构造函数私有
- 定义一个单例静态成员,静态成员在程序运行之前完成初始化
- 提供一个静态方法获取单例静态成员
- 防拷贝
class singleton1 {
public:
static singleton1* getinstance() {
return &m_instance;
}
private:
// 1. 构造函数私有
singleton1() {};
// 2. 采用 c++11删除函数 拷贝函数、赋值运算符私有
singleton1(singleton1 const&) = delete;
singleton1& operator=(singleton1 const&) = delete;
static singleton1 m_instance;
};
singleton1 singleton1::m_instance;
下面为单线程懒汉模式:
// 单线程懒汉模式
class Singleton
{
public:
static Singleton* getInstance() {
// 提高后续线程调用接口的效率
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
return _sin;
}
private:
Singleton(){}
Singleton(const Singleton& s) = delete;
static Singleton* _sin; // 定义为指针,与对象不为同一类型,其为单独的指针类型
};
Singleton* Singleton::_sin = nullptr;
上述单线程懒汉模式代码在Singleton
的静态函数getInstance
中,只有在_sin
为nullptr
的时候才创建一个实例以避免重复创建。同时我们封死构造函数、拷贝构造、赋值运算符,这样就能确保只创建一个实例。.
3.3 不好的解法二:虽然在多线程环境中能工作但效率不高
解法一中的懒汉模式代码在单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断getInstance
是否为 nullptr
的 if
语句,并且_sin
的确没有创建时,那么两个线程都会创建一个实例,此时类型 Singleton
就不再满足单例模式的要求了。为了保证在多线程环境下我们还是只能得到类型的一个实例,需要加上一个同步锁。把 Singleton
稍做修改得到了如下代码:
// 单线程懒汉模式
#include <mutex> // 加锁头文件,互斥锁,所有的线程共用同一把锁,全局只有一把锁,用一把锁限制所有线程
class Singleton {
private:
static mutex _mtx; // 全局只有一把锁,限制全部线程
public:
static Singleton* getInstance() {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
private:
// 1. 构造函数私有化 2. 拷贝构造私有化(不必实现) 3. 赋值运算符无所谓私有化,因为其不创建新的对象
Singleton(){}
Singleton(const Singleton& s) = delete;
static Singleton* _sin; // 定义为指针,与对象不为同一类型,其为单独的指针类型
};
Singleton* Singleton::_sin = nullptr;
3.4 可行的解法:加同步锁前后两次判断实例是否已存在
我们只是在实例还没有创建之前需要加锁操作,以保证只有一 一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。于是我们可以把解法二中的代码再做进一步的改进:
// 单线程懒汉模式
#include <mutex> // 加锁头文件,互斥锁,所有的线程共用同一把锁,全局只有一把锁,用一把锁限制所有线程
class Singleton {
private:
static mutex _mtx; // 全局只有一把锁,限制全部线程
public:
static Singleton* getInstance() {
if (_sin == nullptr) {
_mtx.lock(); // 加锁不能在if内,没有意义,还是要创建对象
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
private:
// 1. 构造函数私有化 2. 拷贝构造私有化(不必实现) 3. 赋值运算符无所谓私有化,因为其不创建新的对象
Singleton(){}
Singleton(const Singleton& s) = delete;
static Singleton* _sin; // 定义为指针,与对象不为同一类型,其为单独的指针类型
};
Singleton* Singleton::_sin = nullptr;
3.4 中只有当 _sin
为 nullprtr
即没有创建时,需要加锁操作。当 _sin
已经创建出来之后,则无须加锁。因为只在第一次的时候 _sin
为 nullptr
,因此只在第一次试图创建实例的时候需要加锁。这样 3.4 的时间效率比 3.3 要好很多。
3.4 中用加锁机制来确保在多线程环境下只创建一个实例,并且用两个 if
判断来提高效率,实现Double-check,这个是很重要的点 。这样的代码实现起来比较复杂,容易出错,我们还有更加优秀的解法。
3.5 强烈推荐的解法一:利用静态构造函数
在 C#
有静态构造函数的写法,在此我也没学习过 C#
,故不作讨论,可参见书本的 P34-强烈推荐的解法一:利用静态构造函数中所讲。在此主要关注 强烈推荐的解法二:实现按需创建实例。
3.6 强烈推荐的解法二:内部类写法
原书中的写法时基于 3.5C#
中利用静态构造函数进行的内部类写法,在此没办法对其进行拓展。但在 C++
中恰好可以通过内部类写法来手动释放内存,更加的巧妙和精细的进行了安全的内存管理,这是每一个 C++
程序员所希望的!下面来看看实现的思路及代码:
在调用 getInstance
时,用 new
申请了空间,但用完我们并没有释放空间。现在,也不需要手动去释放,单例不仅仅在一个地方使用,可能也在其它地方使用,释放了会导致程序崩溃。
Singleton* ps = Singleton::GetInstance();
delete ps;
ps = nullptr;
所以我们只能 delete ps
,再将 ps
置空,但是,将 ps
空间释放之后,类中的空间又没有被释放,还是一个有效值。 而且不光在此使用这个空间,在其他的地方到该空间的接口,一开始调用的时候该指针有效,但在此已经被释放了,会出现解引用的错误。不能手动去释放。
在此,一般可以不用管,因为其为静态成员,在整个程序运行周期内均有效,程序运行结束即进程结束,那么会将所有的空间资源均返还给系统,达到垃圾回收的目的。
但是若是想手动释放的话,可以在内部定义一个内部类辅助操作。内部类可以访问外部类的私有成员,并且可以直接访问。
class Singleton {
public:
static Singleton* getInstance() {
if (_sin == nullptr) {
_mtx.lock();
if (_sin == nullptr) { // 第一次为空,创建对象,第二次非空,直接返回,保证单例
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
class GC { // 定义内部类,进行垃圾回收
public:
~GC() {
if (_sin) {
delete _sin;
_sin = nullptr;
}
}
};
private:
Singleton(){}
Singleton(const Singleton& s) = delete;
static Singleton* _sin;
static mutex _mtx;
static GC _gc;
};
Singleton* Singleton::_sin = nullptr;
mutex Singleton::_mtx;
Singleton::GC Singleton::_gc; // 它是静态成员,其生命周期也是整个程序的生命周期,调用析构函数释放空间
为什么要采用内部类来做这样一个事情呢?为什么不能在单例上直接写析构函数进行资源回收呢?
class Singleton {
public:
static Singleton* getInstance() {
if (_sin == nullptr) {
_mtx.lock();
if (_sin == nullptr) {
_sin = new Singleton;
}
_mtx.unlock();
}
return _sin;
}
~Singleton() { // 单例中析构函数产生递归效果
if (_sin) { // 之前所产生的对象不为当前类,不会重复递归调用析构函数
delete _sin;
_sin = nullptr;
}
}
~Singleton
,会在 delete _sin
上重复调用析构函数产生递归效应。因为之前调用析构函数释放的资源不是当前类类型的,不会去递归调用当前类的析构函数,而再次调用刚好触发了该条件。 写这么多,在C++
中难道它智能指针
不香吗~~~
在此挖个坑,待填~~