单例模式(Singleton)
一、 什么是单例模式
单例模式,简单点来说就是设计一个类,使其在任何时候,最多只有一个实例,并提供一个访问这个实例的全局访问点。
二、 为什么要单例
在程序中的很多地方,只有一个实例是非常重要的。例如,在windows中,任务管理器只有一个,无论你点击多少次打开任务管理器,任务管理器也只会生成一个窗口。再例如,在一些软件中,工具箱是唯一的,无论你点击多少次打开工具箱,工具箱也只一个。
为什么要这样设计呢?因为像任务管理器或工具箱这样的程序,只要有一个就足够完成所有的工作了,多个程序只会白白消耗系统资源,而像任务管理器这类的程序还会引入多个任务管理器之间的同步问题,所以对些这些程序来说,只有一个实例或程序是必要的。
三、 为什么需要单例模式
上面讲到对于某些程序来说,保持其只有一个实例是必要的,但是如何保证一个程序或一个类只有一个实例呢?下面从类的角度来解说。
第一种方法,我们抛开设计模式这个概念,如果你之前完全不知道这个概念,面对这个设计要求你会怎样做?我们可以使用一个全局的类指针变量,初始值为NULL,每当需要创建该类的对象时,都检查该指针是否为NULL,若为NULL,则使用new创建新的对象,并把对象的指针赋值给该全局指针变量。若该指针不为NULL,则直接返回该指针或使用该指针。这个可能是最容易想到的方法。
第二种方法,就是使用单例模式。单例模式通过在类内维护一下指向该类的内部的指针,并把其构造函数声明为private或protected来阻止一般的实例化,而使用一个static的公有成员函数来实现和控制类的实例化。在该static公有成员函数中判断该类的静态成员指针是否为NULL,若为NULL,则创建一个新的实例,并把该类的静态成员指针指向该实现。若该静态成员指针不为NULL,则直接返回NULL。若这里看得不是很明白,不要紧,看了下面的类图和代码自会明白。
相比之下,第二种方法比第一种方法好在哪里呢?首先,第一种做法并没有强制一个类只能有一个实例,一切的控制权其实在使用者的设计中;而第二种做法,则是类的设计者做好的,与使用者并没有关系。换句话来说,如果使用第一种做法,则只要使用者愿意,该类可以有无数个实例,而对于第二种方法,无论使用都是否愿意,它只能有一个实例。这就好比我们去吃饭,第一种方法需要顾客来判断哪些菜已经卖完,而第二种方法由餐馆的判断哪些菜已经卖完,显然在生活中,第二种方法才是合理的。
四、 单例模式的类图
五、 单例模式的实现(C++实现)
1、singleton.h,定义类的基本成员及接口
#ifndef SINGLETON_H_INCLUDE
#define SINGLETON_H_INCLUDE
class Singleton
{
public:
static Singleton*getInstance();
voidreleaseInstance();
private://function
Singleton(){}
~Singleton(){}
private://data
static Singleton*_instance;
static unsigned int_used;
};
#endif
2、singleton.cpp,实现getInstance方法和releaseInstance方法
#include "singleton.h"
Singleton* Singleton::_instance(0);
unsigned int Singleton::_used(0);
Singleton* Singleton::getInstance()
{
++_used;
if(_instance == 0)
{
_instance = newSingleton();
}
return _instance;
}
void Singleton::releaseInstance()
{
--_used;
if(_used == 0)
{
delete _instance;
_instance = 0;
}
}
代码分析:
从上面的类图和代码实现可以看到,在单例模式中,我们把类的构造函数声明为私有的,从而阻止了在类外实例化对象,既然在类外不能实例化对象,那么我们如何实例化该类呢?我们知道static成员是随类而存在的,并不随对象而存在,所以我们利用一个公有的static函数,由它来负责实现化该类的对象,因为该static函数是该类的成员函数,它可以访问该类的private的构造函数,它也就是我们之前所说的全局访问点。
由于可能有多个对象都引用该单例类的对象,而该对象只有一个,所以肯定会有多个指针变量指向堆中同一块内存,若其中一个指针把该堆内存delete掉,然而其他的指针并不知道它所引用的对象已经不存在,继续引用该对象必然会发生段错误,为了防止在类的外部调用delete,在这里把析构函数声明为private,从而让在类外delete一个指向该单例类对象指针的操作非法。
但是C++的堆内存完全由程序员来管理,如果不能delete的话,该对象就会在堆内存中一直存在,所以在此引入了一个方法releaseInstance和引用计数,当不再需要使用该对象时调用releaseInstance方法,该方法会把引用计数减1,当所有代码都不需要使用该对象时释放该对象,即当引用计数为0时,释放该对象。
六、 多线程下的单例模式
上面的代码在多线程环境下会引发问题,举个例子,就是当两个线程同时调用getInstance函数时,若该类还没有被实例化,则两个线程读取到的_instance为0,那么两个线程都会new一个新的对象,从而让该类有两个实例,同时,对_used的操作也会存在相同的问题,此时_use为1。所以,显然该设计在多线程下是不安全的。
为了解决上述问题,我们需要为函数getInstance和releaseInstance中对_instance和_used的访问加锁。为了简便,只列出部分关键代码,修改后的代码如下所示:(源代码文件为singlton_thread.h和singleton_thread.cpp)
pthread_mutex_tSingleton::_mutex(PTHREAD_MUTEX_INITIALIZER);
Singleton* Singleton::getInstance()
{
pthread_mutex_lock(&_mutex);
++_used;
if(_instance== 0)
{
_instance= new Singleton();
}
pthread_mutex_unlock(&_mutex);
return_instance;
}
void Singleton::releaseInstance()
{
pthread_mutex_lock(&_mutex);
--_used;
if(_used== 0)
{
delete_instance;
_instance= 0;
}
pthread_mutex_unlock(&_mutex);
}
代码分析:
从上面的代码可以看出,每次申请调用get/releaseInstance函数都会加锁和解锁,而加锁和解锁都是比较耗时的操作,所以上述的代码效率其实并不高。
在一些设计模式的书上,会看到使用双if的判断来解决多次上锁的问题,但是这个方法在这里是不现实的,因为这个方法不能解决_used的访问问题,也就是说,即使对_instance的访问可以使用双if语句来大大减少加锁和解锁的操作,但是对_used的++和--操作同样需要加锁进行。而那些书上之所以可以使用双if来解决这个问题,是由所使用的语言决定的,例如使用java或c#,它们不需要对内存进行管理,所以不会存在上面代码中所出现的引用计数_used,所以双if的方法才行得通。
若想在C++中实现双if的判断,则不使用引用计数来管理内存即可,即对象一旦分配就一直存在于堆内存中。此时C++也不存在引用计数问题,不需要释放内存,因而也就不需要上面的releaseInstance方法。其getInstance方法的实现如下:
Singleton* Singleton::getInstance()
{
if(_instance == 0)
{
pthread_mutex_lock(&_mutex);
if(_instance == 0)
_instance = new Singleton();
pthread_mutex_unlock(&_mutex);
}
return _instance;
}
这样就可以只加锁和解锁一次,大大提高时间效率。但是对象一旦分配内存,内存就不会被释放。所以在C++中使用哪种实现策略,取决于你对时间和空间的取舍,若时间更重要,则采用后一种方法,若空间更重要,则采用前一种方法。
七、 Android中的单例模式
Android中存在着大量的单例类,如:InputMethodManager类,CalendarDatabaseHelper类、Editable类等等。在这些类中,都存在一个方法getInstance,在该方法或直接返回对象的引用或判断一个类的引用是否为NULL,若不为NULL,则直接返回该引用,若为NULL,则new一个新的对象,并返回。例如,对于CalendarDatabaseHelper类,存在如下的代码:
public static synchronized CalendarDatabaseHelper getInstance(Contextcontext)
{
if (sSingleton == null)
{
sSingleton = newCalendarDatabaseHelper(context);
}
return sSingleton;
}
从这里的代码可以看出,其实现方式与上面所说的非常相似,不过因为java不用程序员自己管理内存,所以并不需要使用引用计数,而该方法是公有static的。而synchronized就是为了保证同一时刻只能有一个线程进入该方法,这也就是防止上面第六点讲到的单例模式在多线程中的安全问题。
八、 源代码地址
C++源代码地址:http://download.csdn.net/detail/ljianhui/7464147