设计模式系列(八)单例模式(Singleton Pattern)
单例模式就是确保一个类只有一个实例,并提供一个全局的访问点。具体来看,就是指定义的某个类,在程序运行期间,只允许有一个实例对象,即在内存中仅仅有一块内存用来存储这个类的唯一的实例对象,程序运行期间不允许出现第二个该类的实例对象。
很多会问,为什么定义了一个类却只实例化一个对象。这个主要是因为,有时候在实际应用中,某些事物只希望存在一个对象,比如说大家电脑中的注册表对象,如果存在多个注册表对象,那么会引起很多不一致且难以维护的问题,故应该只保持一个对象在被操作。还有很多其他的例子,例如配置文件、线程池、缓存、对话框等,这些都只需要一个对象来管理。
既然要求只能存在一个对象,那么如何做呢?一般需要采用以下要求:
(1)类的构造函数必须为私有的,防止在类外被实例化;
(2)在C++中拷贝构造函数和赋值操作也应该是私有的,防止对象被拷贝和赋值;
(3)在类的内部产生一个唯一的对象并提供全局访问点,以保持该类只存在一个对象且被全局访问。
单例模式是一种看起来比较简单的设计模式,但是其存在很多细节问题,这些问题也是十分重要的,所以不要小瞧单例模式的实现,尤其是在C++中的实现。
单例模式可以分为懒汉式和饿汉式两种,然后还可以根据是否线程安全来对这两种方式进行同步,但是要注意同步对程序运行效率的影响。
1. 懒汉式(完善版)
下面我们先来看看懒汉式单例模式的例子,这里有三个文件,请看例子中的注释。
// 单例模式
// SingletonPattern.h文件
#ifndef SINGLETON
#define SINGLETON
#include <iostream>
#include <iomanip>
#include <string>
using std::string;
using std::cout;
using std::endl;
// 单例模式第一种形式:懒汉式
class Singleton
{
public:
static Singleton* getInstance();
void printTest();
private:
static Singleton* uniqueInstance;
// 不允许在外部新建对象、拷贝对象、赋值对象
Singleton(){}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
// 内部类,用来在程序结束后释放唯一的实例指针
class InnerDelete
{
public:
~InnerDelete()
{
cout << "内部类的析构函数被调用" << endl;
if (Singleton::uniqueInstance != NULL)
{
delete Singleton::uniqueInstance;
Singleton::uniqueInstance = NULL;
}
cout << "-------------------------------------" << endl;
}
};
// 必须定义一个内部类的静态对象
static InnerDelete innerDelete;
};
#endif
// SingletonPattern.cpp文件
#include "SingletonPattern.h"
// 单例模式第一种形式:懒汉式
Singleton* Singleton::uniqueInstance = NULL;
Singleton* Singleton::getInstance()
{
if (uniqueInstance == NULL)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
void Singleton::printTest()
{
cout << "这是单例模式的服务函数" << endl;
}
Singleton::InnerDelete Singleton::innerDelete;
// SingletonPatternTest.cpp文件
#include "SingletonPattern.h"
void main()
{
// 直接通过类名获取对象指针
cout << "-------------------------------------" << endl;
Singleton* st = Singleton::getInstance();
cout << "懒汉式单例模式对象获取" << st << endl;
st->printTest();
// 直接通过类名获取对象指针
cout << "-------------------------------------" << endl;
Singleton* st1 = Singleton::getInstance();
cout << "懒汉式单例模式对象获取" << st1 << endl;
st1->printTest();
// 通过已经获取的对象指针来获取对象指针
cout << "-------------------------------------" << endl;
Singleton* st2 = st1->getInstance();
cout << "懒汉式单例模式对象获取" << st2 << endl;
st2->printTest();
// 通过已经获取的对象指针来获取对象引用
cout << "-------------------------------------" << endl;
Singleton& st3 = *Singleton::getInstance();
cout << "懒汉式单例模式对象获取" << &st3 << endl;
st3.printTest();
cout << "-------------------------------------" << endl;
}
该例的运行结果如图1所示,UML类图如图2所示。
图1 懒汉式运行结果
图2 懒汉式UML类图
这里需要对一些细节进行说明,请大家务必注意:
(1)类的构造函数必须是私有的,至于拷贝构造函数和赋值操作符是否声明为私有,并不是必须的,但是建议加上;
(2)我们在类内部只是声明了静态变量,记住,只是“声明”了,所以一定要在类外对静态变量进行定义,这里在SingletonPattern.cpp文件中有两行是做这样的操作,即
Singleton* Singleton::uniqueInstance = NULL;
Singleton::InnerDelete Singleton::innerDelete;
(3)既然我们在类内部new了一个对象,那么这块内存虽然只有一块,但是不进行delete的话会造成内存泄露,常规的单例模式例子中大家基本上很少看到去释放内存的,这一点尤其容易被忽略,那么到底如何释放内存呢?有些人可能认为可以在类外让客户调用Singleton::getInstance(),然后delete这个指针,这个办法虽然可行,但是明显是不好的选择,让客户去考虑释放指针的问题就显得太不友好了。所以本例采用嵌套类的方式进行内存释放。应该清楚:程序在结束的时候,系统会自动析构所有的全局变量,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样,所以我们通过在单例模式的类中定义一个嵌套类,然后只在该类中定义其析构函数,用来释放单例的指针内存。但是一定要注意!!!一定要在单例类中声明该嵌套类的一个静态变量,然后在类外还要去定义这个静态变量。特别注意,在类外定义:“ Singleton::InnerDelete Singleton::innerDelete; ”这条语句是必不可少的,否则析构函数不会被调用。请注意图1中的显示结果,显示出了析构函数被调用。
(4)单例模式的类中还可以有其他的函数和成员变量,这里只是给出了单例模式最基本的成员,该例中定义了一个printTest()函数,就是该单例类的操作之一。
(5)在SingletonPatternTest.cpp文件中,大家要注意获取这个唯一实例的方法,并且注意多种获取方式都可以。大家可以看到图1中的运行结果,不管进行多少次获取,这个对象的地址都是一样的,也就是说只有一个实例对象存在。
最后,我们可以说懒汉式是采用了延迟实例化的方法,在需要对象的时候才实例化。
2. 饿汉式
在上面的懒汉式中,已经详细分析了单例模式的各种细节问题。对于后面介绍的例子就不再给出详细的文件列表和UML类图及测试,大家可以自行按照上面的形式进行测试完善,这些都是类似的,当作一种练习,正好锻炼下。
下面我们来看看饿汉式的程序。
// 单例模式第二种形式:饿汉式
class Singleton1
{
public:
static Singleton1* getInstance();
void printTest();
private:
static Singleton1* uniqueInstance;
// 不允许在外部新建对象、拷贝对象、赋值对象
Singleton1(){}
Singleton1(const Singleton1&);
Singleton1& operator=(const Singleton1&);
// 内部类,用来在程序结束后释放唯一的实例指针
class InnerDelete
{
public:
~InnerDelete()
{
cout << "内部类的析构函数被调用" << endl;
if (Singleton1::uniqueInstance != NULL)
{
delete Singleton1::uniqueInstance;
Singleton1::uniqueInstance = NULL;
}
cout << "-------------------------------------" << endl;
}
};
// 必须定义一个内部类的静态对象
static InnerDelete innerDelete;
};
// 单例模式第二种形式:饿汉式
Singleton1* Singleton1::uniqueInstance = new Singleton1();
Singleton1* Singleton1::getInstance()
{
return uniqueInstance;
}
void Singleton1::printTest()
{
cout << "这是单例模式的服务函数" << endl;
}
Singleton1::InnerDelete Singleton1::innerDelete;
从上面的程序中可以看出,饿汉式和懒汉式的区别就在于:饿汉式在下面语句中直接new了一个对象出来,也就是说在定义的时候直接定义了一个实例对象,而不管目前是否需要这个实例对象,其他的部分与懒汉式基本是一样的。
Singleton1* Singleton1::uniqueInstance = new Singleton1();
从上面可以看出,懒汉式和饿汉式就是制造这个唯一对象的时机不一样,懒汉式是在需要的时候才通过函数new,而饿汉式是在定义的时候直接new。
3. 多线程单例模式中线程安全问题
上述例子在一个线程的情况下运行没有问题,但是在多线程的情况下可能会出现不止一个对象的问题。比如说:一个线程刚执行if (uniqueInstance == NULL)这个判断,而且此时还没有创建对象,在这个线程正准备创建对象的时候,另外一个线程抢夺了资源,也执行if (uniqueInstance == NULL),然后创建了一个对象并返回,此时第一个线程又抢夺回了资源,由于其已经执行过了if (uniqueInstance == NULL)这个判断,所以虽然已经有别的线程创建了对象,但是其并不知道,所以这个线程也创建了一个对象,从而出现了单例模式的单例类的两个对象,从而引发一些后续的错误。
这属于多线程同步的问题,接触过多线程的朋友应该了解这些基本的概念。那么如何处理呢?在C++11以前还是略显麻烦的,需要使用网上的开源库比如boost库,或者自己写一个互斥锁的相关类,而C11提供了这些类的实现,不过目前采用C11的应该并不是很多,所以考虑到具体的实现并不确定,这里只给出基本的实现思路。
既然知道了是因为在单例类中的getInstance()函数中引起的同步问题,所以对于多线程的处理只需要对这个函数进行同步或者对函数内部的不确定的变化的代码块进行同步即可,JAVA里面实现起来很方便,也有多种实现方式:直接对方法进行同步或进行双重检验同步等,而在C++中也可以采用类似的方法,不过C11以前没有标准库来使用,所以需要自己实现或者下载开源库,下面是一个简单的示例代码,并不能直接运行,仅供参考理解。
Singleton* Singleton::getInstance()
{
// 在这里进行上锁,通过互斥锁实现,如mutex锁,调用lock()实现上锁
if (uniqueInstance == NULL)
{
uniqueInstance = new Singleton();
}
// 在这里进行解锁,通过互斥锁实现,如mutex锁,调用unlock()实现解锁
return uniqueInstance;
}
按照上述注释的位置进行上锁和解锁即可,当然还有其他的方法,比如说双重校验,即对函数同步,然后在函数内部上锁后再次调用if (uniqueInstance == NULL)来判断,从而决定是否创建对象,具体的细节问题大家可以在使用时根据需求进行相应的实现。此处不再详述。
4. 总结
单例模式虽然看起来很简单,但是却有很多很多细节问题需要注意,所以大家一定要细心处理,好好研究下单例模式,有什么建议或问题欢迎交流。