创建型模式之单例模式

模式定义

  单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

  单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

模式动机

  对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

模式结构

单例模式包含如下角色:

  • Singleton:单例
    单例模式图片

C++单例的实现

基础要点

  • 意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 主要解决: 一个全局使用的类频繁地创建与销毁。
  • 何时使用: 当您想控制实例数目,节省系统资源的时候。
  • 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码: 构造函数是私有的。

懒汉式

//懒汉式的方法是直到使用时才实例化对象,也就说直到调用getInstance() 方法的时候才 new 一个单例的对象。好处是如果被调用就不会占用内存。
class Singleton_lazy {
private:
	Singleton_lazy() {
		cout << "Singleton_lazy() 构造函数被调用!" << endl;
	}
public:
	static Singleton_lazy* getInstance() { //公共接口
		if (pSingleton == nullptr) {
			pSingleton = new Singleton_lazy;
		}
		return pSingleton;
	}
private:
	static Singleton_lazy* pSingleton; //静态成员变量所有类共享一份
};
Singleton_lazy* Singleton_lazy::pSingleton = nullptr;

void test1() {
	Singleton_lazy* s1 = Singleton_lazy::getInstance();
	Singleton_lazy* s2 = Singleton_lazy::getInstance();
}

上述代码输出为

Singleton_lazy() 构造函数被调用!

  由此可见,两次获取一个对象,只调用了一次构造函数,最基本的单例模式实现了。但是它是有哪些问题呢?

  1. 线程安全的问题,当多线程获取单例时有可能引发竞态条件:第一个线程在 if 中判断 pSingleton 是空的,于是开始实例化单例;同时第 2 个线程也尝试获取单例,这个时候判断 pSingleton 还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁

  2. 内存泄漏. 注意到类中只负责 new 出对象,却没有负责 delete 对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。解决办法: 使用共享指针,嵌套垃圾回收;

  关于线程安全问题,加一把锁就能解决,此处不做过的多赘述,详情请见 C++ 11 互斥量简介
  其实就单例模式而言,回收内存意义不大,因为在所有程序中该对象只有一份,但是聊胜于无,既然想要回收那就不能显式调用 delete 去回收,因为有可能其他人在使用的时候不小心进行了释放,导致产生莫名的 BUG,所以设计嵌套类的目的是为了定义它的静态子对象,在程序结束时会调用该子对象的析构函数以释放唯一的实例。

使用嵌套类 + 锁实现的单例模式

class Singleton_lazy {
private:
	Singleton_lazy() {
		cout << "Singleton_lazy() 构造函数被调用!" << endl;
	}

	class Garbo {   //嵌套类,它的唯一工作就是在析构函数中释放实例
	public:
		~Garbo() {
			if (Singleton_lazy::pSingleton != nullptr) { //可以在这里销毁所有的资源
				delete Singleton_lazy::pSingleton;
				cout << "Singleton_lazy() 被释放!" << endl;
				Singleton_lazy::pSingleton = nullptr;
			}
		}
	};

public:
	static Singleton_lazy* getInstance() {
		//在判断前,使用锁来保证线程安全,该锁自动加锁,自动解锁
		if(pSingleton == nullptr){ //双检锁
			lock_guard<mutex> lk(m_mutex);
			if (pSingleton == nullptr) {
				pSingleton = new Singleton_lazy;
			}
		}
		return pSingleton;
	}
private:
	static Singleton_lazy* pSingleton;
	static Garbo garbo;//定义一个子对象,在程序结束时会调用它的析构函数
	static mutex m_mutex;//定义一把锁,用来保证线程安全
};
Singleton_lazy* Singleton_lazy::pSingleton = nullptr;
Singleton_lazy::Garbo Singleton_lazy::garbo;
mutex Singleton_lazy::m_mutex;

void test1() {
	Singleton_lazy* s1 = Singleton_lazy::getInstance();
	Singleton_lazy* s2 = Singleton_lazy::getInstance();
}

使用智能指针 + 锁实现的单例模式

  也可以使用C++ 11 中的智能指针 share_ptr 进行解决,当智能指针在被释放会自动释放自己占据的内存,请见智能指针

class Singleton_lazy {
private:
	Singleton_lazy() {
		cout << "Singleton_lazy() 构造函数被调用!" << endl;
	}

public:
	using Ptr = shared_ptr<Singleton_lazy>;  //使用智能指针

	static Ptr getInstance() {  //第一处更改,返回智能指针
		if(pSingleton == nullptr){  //双检锁,避免重复加锁
			lock_guard<mutex> lk(m_mutex);
			if (pSingleton == nullptr) {
				pSingleton = shared_ptr<Singleton_lazy>(new Singleton_lazy); //第二处更改,创建智能指针对象
			}
		}
		return pSingleton;
	}
private:
	static Ptr pSingleton; //第三处更改
	static mutex m_mutex;
};
Singleton_lazy::Ptr Singleton_lazy::pSingleton = nullptr;  //第四处
mutex Singleton_lazy::m_mutex;

void test1() {
	Singleton_lazy::Ptr s1 = Singleton_lazy::getInstance(); //第五处
	Singleton_lazy::Ptr s2 = Singleton_lazy::getInstance();
}

shared_ptrmutex 都是 C++ 11的标准,以上这种方法的优点是:

  1. 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏
  2. 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

  不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。

因此这里还有第三种的基于 Magic Staic的方法达到线程安全

基于局部静态变量的单例模式(magic static )

class Singleton_lazy {
private:
	Singleton_lazy() {
		cout << "Singleton_lazy() 构造函数被调用!" << endl;
	}
public:
	static Singleton_lazy& getInstance() {
		static Singleton_lazy pSingleton;
		return pSingleton;
	}
	~Singleton_lazy() {
		cout << "Singleton_lazy() 析构函数被调用!" << endl;
	}
private:
};

void test1() {
	Singleton_lazy& s1 = Singleton_lazy::getInstance();
	Singleton_lazy& s2 = Singleton_lazy::getInstance();
}

上述代码输出为

Singleton_lazy() 构造函数被调用!
Singleton_lazy() 析构函数被调用!

这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。

这是最推荐的一种单例实现方式:

  1. 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
  2. 不需要使用共享指针,代码简洁,不需要担心内存释放的问题;
  3. 注意在使用的时候需要声明单例的引用 Singleton_lazy& 才能获取对象。

饿汉式

饿汉式是一个线程安全的写法,但是由于先初始化,所以会产生垃圾对象。
优点:没有加锁这一步,执行效率提高
缺点:在代码执行前就加载好了,浪费内存
比较简单也没什么好说的。。。

class Singleton_hungry {
private:
	Singleton_hungry() {}
	static Singleton_hungry* getInstance() { return pSingleton; }
private:
	static Singleton_hungry* pSingleton;
};
Singleton_hungry* Singleton_hungry::pSingleton = new Singleton_hungry;

C++ 单例模式总结与剖析

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://neo00.top/archives/创建型模式之单例模式