单例模式
定义
单例模式是指在内存中只会创建且只创建一次对象的设计模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
比如说Windows的任务管理器,只能打开一个,使得能够避免因打开多个任务管理器而造成内存资源的浪费,或出现各个窗口显示内容不一致等错误。
要点
为了防止用户多次实例化对象,可以把类构造函数声明为private,然后通过public函数调用构造函数实现实例化,使得实例化过程让类本身来操作,不受用户影响,实现在内存中只会创建且只创建一次对象的单例模式。具体URL图如下所示:
代码实现
Singleton:
class Singleton
{
private:
// cannot equal to NULL
// static Singleton *instance = NULL;
static Singleton *instance;
Singleton()
{
cout << "constructor called!\n";
}
Singleton(Singleton&){
}
public:
static Singleton *getInstance()
{
if (instance == NULL)
{
instance = new Singleton();
cout << "create object!" << endl;
}
return instance;
}
~Singleton()
{
cout << "destructor called!" << endl;
}
};
//静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
Singleton *Singleton::instance = NULL;
Client:
int main()
{
Singleton *s = Singleton::getInstance();
return 0;
}
在多线程的程序中,多个线程同时访问Singleton类,调用GetInstance()方法,会有可能跟造成多个实例,此时引出一个话题——多线程安全。
用户通过加锁来使得线程位于代码的临界区,另一个代码不得进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待,直到该对象被释放。
第一种加锁模式:
class Singleton
{
private:
// cannot equal to NULL
// static Singleton *instance = NULL;
static Singleton *instance;
Singleton()
{
cout << "constructor called!\n";
}
~Singleton()
{
pthread_mutex_destroy(&mutex); // 释放锁
}
class CRelease
{
public:
~CRelease() { delete single; }
};
static CRelease release;
// 定义线程间的互斥锁
static pthread_mutex_t mutex;
public:
static Singleton *getInstance()
{
// 获取互斥锁
pthread_mutex_lock(&mutex);
if (instance == NULL)
{
instance = new Singleton();
cout << "create object!" << endl;
}
// 解锁
pthread_mutex_unlock(&mutex);
return instance;
}
~Singleton()
{
cout << "destructor called!" << endl;
}
};
//静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
Singleton *Singleton::instance = NULL;
CSingleton::CRelease CSingleton::release;
// 互斥锁的初始化
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;
在判断前加锁的方式效果很明显,适合大部分场景。但是,不足的地方在于,若对象已经创建了,但是当多个用户并发访问时,还是只能有一个用户通过释放锁后才允许第二个用户通过。
可以把这个问题抽象为读写者问题,创建对象操作为写者,而非创建对象操作为读者,这样的加锁方式实际上把所有用户都转化为写者,使得每次访问都需要加锁,并行效率有所下降。
于是提出了第二种加锁方式:双重锁定:
class Singleton
{
private:
// cannot equal to NULL
// static Singleton *instance = NULL;
static Singleton *instance;
Singleton()
{
cout << "constructor called!\n";
}
~Singleton()
{
pthread_mutex_destroy(&mutex); // 释放锁
}
class CRelease
{
public:
~CRelease() { delete single; }
};
static CRelease release;
// 定义线程间的互斥锁
static pthread_mutex_t mutex;
public:
static Singleton *getInstance()
{
if(instance == NULL){
// 获取互斥锁
pthread_mutex_lock(&mutex);
if (instance == NULL)
{
instance = new Singleton();
cout << "create object!" << endl;
}
// 解锁
pthread_mutex_unlock(&mutex);
}
return instance;
}
~Singleton()
{
cout << "destructor called!" << endl;
}
};
//静态成员初始化与一般数据成员初始化不同。静态数据成员初始化的格式如下:
Singleton *Singleton::instance = NULL;
CSingleton::CRelease CSingleton::release;
// 互斥锁的初始化
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;
在加锁前先进行一次判断,在加锁后再进行以此判断,这样的操作称为 Double-Check Locking(双重锁定)。这里面涉及的细节很有意思(为什么要多加一个判断,判断的位置为什么是这样安排),仔细思考才能体会到其中的含义。
二重锁定并不是完美的,C++中编译器有时为了优化编译过程,可能会打破原有的类变量声明定义的过程(分配内存-调用构造器-把地址赋值给变量),引起数据竞争(分配内存-把地址赋值给变量-调用构造器),导致二重锁定失效,此时采用volatile关键字即可解决该问题。volatile关键字使得编译器不会改变指令执行顺序,在多线程中特别有用。
上面所说的单例模式处理方式是再第一次被引用时,才会将自己实例化,所以被称为懒汉式单例类。
还有一种静态初始化的方式,即在加载时就将自己实例化,所以被形象地称为饿汉式单例类。
优点和缺点
优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 基于单例模式我们可以进行扩展,允许可变数目的实例。使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题
缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
应用场景
在以下情况下可以考虑使用单例模式:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径进行访问。