一篇就懂设计模式:单例模式
单例模式(Singleton Pattern)
单例模式顾名思义就是类只有一个实例的模式。当在实际场景中只能存在一个唯一对象时,我们无法保证用户只创建了一个实例对象,尤其是在对系统功能的版本迭代过程中,不同的开发人员可能会创建多个实例对象,这与系统的逻辑可能是不符的。因此需要由类限制实例对象创建的行为。单例模式正是这样一种设计模式,在GOF中其定义如下:
保证类仅有一个实例,并提供一个访问它的全局访问点。
那么问题来了,如何在类内限制实例对象的创建?
主要在于两点:(1)构造函数以及类成员对象的访问权限;(2)在创建实例时需要判断是否已经有实例对象创建
基本思路:
- 要限制用户创建对象的行为,用户不能直接调用构造函数实例化对象,因此构造函数要设置为private;
- 由于类只能有一个实例,因此在类内定义一个该类类型的成员变量,此外用static关键字来设置全局特性;
- 提供一个public接口给用户,该接口中会判断是否已经有实例化的对象,若没有则调用构造函数进行对象的实例,由于在创建实例对象时需要对类进行判断,因此不能通过对象调用该接口,因此需要用static修饰;
根据上面的思路,可以得到单例模式的结构图,如下:
接下来给一段代码样例:
//单例模式之懒汉模式
class Singleton
{
private:
static Singleton* s_instance;
Singleton();
public:
static Singleton* GetInstance();
}
Singleton* Singleton::s_instance= nullptr; //初始化
Singleton::Singleton()
{
}
// 用户创建实例
Singleton* Singleton::GetInstance()
{
if(nullptr == s_instance)
{
s_instance= new Singleton();
}
}
上面创建的类可以实现单例模式,这种创建方式被称作懒汉模式。但是如果有多个线程同时调用了GetInstance方法来实例化对象,可能会出现一个线程已经实例化对象,而另外的线程没有及时得知,又重新创建了实例,此时就破坏了“单例”这一特性。因此这种单例模式是线程不安全的。
那么要实现线程安全,最容易想到的方法是加锁,利用双重锁机制设置临界区,使得在多线程情况下仍能保证只能创建了一个实例,修改后的代码如下:
//懒汉模式(双检锁)
class Singleton
{
private:
static Singleton* s_instance;
Singleton();
public:
static std::mutex s_mutex1;
static std::mutex s_mutex2;
static Singleton* GetInstance();
}
Singleton* Singleton::s_instance = nullptr; //初始化
std::mutex Singleton::s_mutex1;
std::mutex Singleton::s_mutex2;
Singleton::Singleton()
{
}
// 用户创建实例
Singleton* Singleton::GetInstance()
{
if(nullptr == s_instance)
{
std::lock_guard<std::mutex> lock1(s_mutex1);
if(nullptr == s_instance)
{
std::lock_guard<std::mutex> lock2(s_mutex2);
instance = new Singleton();
}
}
}
利用双检锁的懒汉模式可以较好地实现单例模式下的线程安全。此外我们看到懒汉模式实例化对象的特点,只有当需要用到实例时才进行实例化。
除了懒汉模式之外,还有一种单例模式,饿汉模式,代码如下:
//单例模式之饿汉模式
class Singleton
{
private:
static Singleton* s_instance;
Singleton();
public:
static Singleton* GetInstance();
}
Singleton* Singleton::s_instance= new Singleton; //初始化
Singleton::Singleton()
{
}
// 用户访问实例
Singleton* Singleton::GetInstance()
{
return s_instance;
}
从上面的代码可以看出,饿汉模式中在全局中就进行了实例化,提前占用了系统资源,而GetInstance只是用户访问该实例的一个接口,因此是线程安全的。那么懒汉模式和饿汉模式都能够实现线程安全,在实际开发过程中要如何选择呢?
懒汉模式:只有在需要使用类实例时才进行实例化,因此在访问量较小时使用,以时间换取空间。
饿汉模式:不管是否需要使用类实例,在全局中就先进行实例化,因此在访问量较大,线程较多的情况下使用,以空间换取时间。
最后给出一个可以直接运行的饿汉模式的实例,可以根据此实例进行一个修改
#include <iostream>
#include <string>
//饿汉模式
class SingletonInstance
{
private:
SingletonInstance() {};
static SingletonInstance *s_instance;
public:
static SingletonInstance* getInstance()
{
return s_instance;
}
};
SingletonInstance* SingletonInstance::s_instance= new SingletonInstance();
int main()
{
SingletonInstance *instance1 = SingletonInstance::getInstance();
SingletonInstance *instance2 = SingletonInstance::getInstance();
if (instance1 == instance2)
{
std::cout << "instance1 & instance2: Same." << std::endl;
}
else
{
std::cout << "instance1 & instance2: Different." << std::endl;
}
}