单例模式--保证一个类仅有一个实例,并提供一个访问他的全局访问点,该实例被程序所有模块共享。

特点
- 通常我们可以让一个全局变量使得一个对象被访问,但不能防止被实例化多个对象。最好方法就是让类自身负责保存它的唯一实例,这个类可以保证没有其他实例可以被创建,并且提供一个访问该实例的方法。
- 单例模式下,有Singleton类封装其唯一实例,这样它可以严格控制客户端怎样访问以及何时访问。简单来说就是对唯一实例的受控访问。
根据处理方式的不同,单例模式可分为:饿汉模式 和 懒汉模式。
饿汉模式:
饿汉模式是通过静态初始化方式,在程序被加载时就实例化创建唯一对象。
class Singleton{
public:
static Singleton& GetSingleton() // 不能传值,只能传指针或者引用
{
cout << "Creat Singleton Object;" << endl;
return pSing;
}
private:
Singleton(){}
Singleton(const Singleton& pS) = delete;
static Singleton pSing;
};
Singleton Singleton::pSing;
int main()
{
Singleton::GetSingleton();
return 0;
}
由于私有化了构造函数、禁用了拷贝构造,所以我们只能通过专门的接口Singleton::GetSingleton()来调用唯一对象。
懒汉模式:
懒汉模式是在被调用时才实例化创建唯一对象。
class Singleton{
public:
static Singleton* GetSingleton()
{
if (pSing == nullptr)
pSing = new Singleton();
cout << "Creat Singleton Object;" << endl;
return pSing;
}
static void DestorySingleton(){
delete pSing;
pSing = nullptr;
cout << "Destory Success;" << endl;
}
private:
Singleton(){}
Singleton(const Singleton& pS) = delete;
static Singleton* pSing;
};
Singleton* Singleton::pSing = nullptr;
int main()
{
Singleton::GetSingleton();
Singleton::DestorySingleton();
return 0;
}
多线程时的单例:
由于饿汉模式下的单例是静态初始化,在程序运行时唯一对象已经创建好,没有在多个线程访问的安全问题。
但懒汉模式下,可能出现多个线程同时访问Singleton类,调用GetSingleton()方法,可能会创建多个实例。
对于处理线程安全问题上,我们可以通过互斥锁来保证:当一个线程位于代码临界区时,另一个线程不进入临界区,如果其他线程试图进入锁住的代码,它会被一直阻塞,直到该对象被释放。
/*
所以创建互斥锁
static mutex m;
*/
static Singleton* GetSingleton()
{
m.lock();
if (pSing == nullptr)
pSing = new Singleton();
cout << "Creat Singleton Object;" << endl;
m.unlock();
return pSing;
}
但是单纯像上面所展示的代码一样,直接加锁,那么每次有线程调用GetSingleton()时,都需要加锁、解锁,而如果已经实例化了对象这样的操作无疑是影响性能的。
改进方式:先进行一次判断,如果已经实例化了对象则,直接返回,不需要等待。
static Singleton* GetSingleton()
{
if (pSing == nullptr){
m.lock();
if (pSing == nullptr)
pSing = new Singleton();
cout << "Creat Singleton Object;" << endl;
m.unlock();
}
return pSing;
}
现在,我们就不用让线程每次都加锁,而只是在实例未被创建时再加锁处理,既提高了效率,也保证了线程安全。这种做法也叫做双重锁定。
指令重排:
指令重排是一种编译器优化方式,通过调整指令执行顺序,来提高性能。
我们在new创建对象时,通常是:
1. 调用operator new,创建内存空间
2. 调用构造函数,初始化对象
3. 将对象的地址赋值给指针变量
在第2步 初始化对象是一个内存操作,这可能是一个耗时操作,为了避免CPU在等待这个操作时停顿导致性能下降,编译器会调整指令顺序(开启优化)或者有些架构的CPU会乱序执行指令,也就是将 3 提前执行,这样的话,指针就指向了一个未初始化好的对象。外部得到的单例对象是一个未初始化好的对象,就会引发问题。
考虑到这点,我们可以通过 volatile关键字声明pSing。
static volatile Singleton* pSing;
volatile 作用:
- 保证内存可见性。(在多线程环境中,任何线程对共享变量的修改,其他线程都是可感知的。)
- 防止指令重排。
总结:
饿汉模式与懒汉模式
饿汉模式:是静态初始化方式,在自己被加载是就将自己实例化;
由于是静态初始化方式,所以程序一加载就需要实例化对象,提前占用系统资源。多线程高并发环境下频繁使用,性能要求较高,那么显然使用饿汉模式来避免资源竞争,提高响应速度更好。
但可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
懒汉模式:在第一次被引用时,才会将自己实例化。
与饿汉模式不同,是一种延迟启动方式,所以程序启动较快。
但由于调用时才实例化,在多线程下存在线程安全问题,需要双重锁定才能保证安全,实现上比较麻烦。