[剑指-Offer] 2. 实现Singleton模式(单例模式、细节处理、代码优化)

1. 题目来源

《剑指-Offer》第二版,P32,面试题2:实现Singleton模式

2. 题目说明

设计一个类,我们只能生成该类的一个实例

3. 题目解析

3.1 单例模式为什么常考?

只能生成一个实例的类是实现了Singleton (单例)模式的类型。设计模式在面向对象程序设计中起着举足轻重的作用,在面试过程中很多公司都喜欢问一些与设计模式相关的问题。在常用的模式中,Singleton 是唯一 一个能够用短短几十行代码完整实现的模式。因此,写一个 Singleton 的类型是一个很常见的面试题。

在博主的[C++系列] 中也对单例模式进行了讲解、实现,可参考以下两篇博文:

[C++系列] 42. 饿汉模式剖析—单例模式
[C++系列] 43. 懒汉模式剖析—单例模式

3.2 不好的解法一:只使用与单线程环境

由于要求只能生成一个实例,因此我们必须把构造函数设为私有函数以禁止他人创建实例。我们可以定义一个静态的实例,静态成员在程序运行之前完成初始化,并提供一个静态方法获取单例静态成员。下面定义类型Singleton1 就是基于这个思路的实现,四大实现要点如下

  1. 构造函数私有
  2. 定义一个单例静态成员,静态成员在程序运行之前完成初始化
  3. 提供一个静态方法获取单例静态成员
  4. 防拷贝
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 中,只有在_sinnullptr的时候才创建一个实例以避免重复创建。同时我们封死构造函数、拷贝构造、赋值运算符,这样就能确保只创建一个实例。.

3.3 不好的解法二:虽然在多线程环境中能工作但效率不高

解法一中的懒汉模式代码在单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断getInstance是否为 nullptrif 语句,并且_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 中只有当 _sinnullprtr 即没有创建时,需要加锁操作。当 _sin 已经创建出来之后,则无须加锁。因为只在第一次的时候 _sinnullptr,因此只在第一次试图创建实例的时候需要加锁。这样 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++难道它智能指针不香吗~~~

在此挖个坑,待填~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值