目录
引入
设想一下,你现在正在使用编译器写代码,此时你点工具箱,它会显示出来一个功能界面,你再点一下,他就会关闭,不会再显示出另一个界面。如图:
它永远不会是这样:
不论你点多少次,它只会显示一次。这其实就是一个单例模式。
单例模式概念
单例模式是指一个类有且仅有一个实例,并提供一个可以访问它的全局访问点,即:在内存中仅会创建一次对象。
就像上图所示的,我们每次点击工具栏后,弹出的页面都是一样的,那么我们就没必要让它弹出很多次。这里的每一个页面我们可以设想为一个对象,如果我们创建很多个对象,但是它们的作用都是相同的,那这是对内存资源的极大浪费(每次创建对象都会开辟内存)。最好的解决办法就是:我们只创建一个对象,并提供一个可以访问它的方法,让所有需要的函数都共享这一对象。
举例:
单例模式的设计类型
单例模式的类型有两种:
- 懒汉式:在真正需要使用时采取创建该对象。
- 饿汉式:在类加载时已经创建好该对象,等着被调用。
当你肚子不太饿时,你去饭店可以等待一下,让厨师给你现做,可以理解为懒汉式;
当你非常饿时,你提前预约,告诉饭店老板,你现在给我做,我马上过去,等你到达时,菜已经提前做好了,可以理解为饿汉式;
饿汉式创建对象
饿汉式创建对象即:在类加载时,单例对象已经被提前创建好了,当程序调用时,直接返回这个对象即可。
代码实现:
class Singleton
{
private:
int value;
static Singleton instance;
private:
Singleton(int x=0):value(x){}
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
public:
static Singleton& GetInstance()
{
return instance;
}
};
Singleton Singleton::instance(10);
int main()
{
Singleton& obja = Singleton::GetInstance();
Singleton& objb = Singleton::GetInstance();
Singleton& objc = Singleton::GetInstance();
Singleton& objd = Singleton::GetInstance();
cout << &obja <<endl;
cout << &objb << endl;
cout << &objc << endl;
cout << &objd << endl;
return 0;
}
运行结果:
饿汉单例模式中,单例对象是通过一个static的静态对象构建的,它在程序启动时,即main函数执行之前,就已经初始化好了,因此不会存在线程安全问题。我们一般多说的线程安全,都是指懒汉模式中的线程安全,稍后会介绍。
懒汉式创建对象
懒汉式创建即:在函数调用该单例对象的时候,先判断这个对象是否已经存在,若存在,直接返回这个对象即可;若还没有创建,就先执行实例化操作。如图:
代码实现如下:
class Singleton
{
private:
int value;
static Singleton* pobj;
private:
Singleton(int x=0):value(x){}
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
public:
static Singleton* GetInstance(int x)
{
if (pobj == NULL)
{
pobj = new Singleton(10);
}
return pobj;
}
};
Singleton* Singleton::pobj = NULL;
int main()
{
Singleton* obja = Singleton::GetInstance(10);
Singleton* objb = Singleton::GetInstance(10);
Singleton* objc = Singleton::GetInstance(10);
Singleton* objd = Singleton::GetInstance(10);
cout << obja <<endl;
cout << objb << endl;
cout << objc << endl;
cout << objd << endl;
return 0;
}
运行结果:
可以看出,不论定义多少对象指针,它们所指的实例对象只有一个。
懒汉式的线程安全
从上述代码我们可以看出,在单线程中,这已经是一个不错的单例模式,肯定只会有一个单例对象。但是在多线程中,它就有问题了。如图:
如果A线程和B线程同时进入if判断语句,那么此时就会产生两个对象,这不符合单例模式的设计思想,这个时候我们首先想到的是加锁,如下:
static Singleton* GetInstance(int x)
{
std::lock_guard<std::mutex>lock(mtx);
if (pobj == NULL)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
pobj = new Singleton(x);
}
return pobj;
}
这种方法解决了线程安全的问题,但是它的效率如何呢?
我们发现,除了第一次实例化对象之外,我们获得的锁有意义外,其它需要直接调用对象的获得的锁好像没有任何意义,我们可否直接将加锁语句放在此if语句里面,如下:
static Singleton* GetInstance(int x)
{
if (pobj == NULL)
{
std::lock_guard<std::mutex>lock(mtx);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
pobj = new Singleton(x);
}
仔细观察就会发现问题,如果多个线程同时进入了if语句,那么不仅会有许多线程等待锁,而且会产生很多实例对象,所以我们不能这样做。正确的解决办法如下:
static Singleton* GetInstance(int x)
{
if (pobj == NULL)
{
std::lock_guard<std::mutex>lock(mtx);
if (pobj == NULL)
{
std::this_thread::sleep_for(std::chrono::milliseconds(10));
pobj = new Singleton(x);
}
}
return pobj;
}
上述代码很好的解决了线程安全和效率的问题,称为双重校验+加锁:
- 如果pobj不为空,则直接返回,不需要获得锁,不会再降低效率;
- 如果有很多线程都进入第一个if语句,那么其中一个线程会获得锁,之后的线程执行到第二个if语句时,也会直接返回,确保了线程安全;
懒汉式单例模式所造成的内存问题
对于懒汉模式,我们说它是在程序调用的时候才创建的(new出来的),这个时候就涉及到内存释放的问题,在多线程情况下,我们很可能会忘记最后的delete,我们希望它能够自己在函数结束时就自动释。
解决办法:static静态成员变量在程序结束时会自行进行内存的释放,我们可以利用这个特点达成上述目标
代码实现如下:
class Singleton
{
private:
int value;
static Singleton* pobj;
private:
Singleton(int x=0):value(x){}
~Singleton(){cout<<"~Singleton()"<<endl;}
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
public:
static Singleton* GetInstance(int x)
{
if (pobj == NULL)
{
pobj = new Singleton(10);
}
return pobj;
}
class Release//定义一个嵌套类
{
public:
~Release()
{
delete pobj;
pobj=NULL;
}
}
static Release release;
};
Singleton* Singleton::pobj = NULL;
Singleton::Release Singleton::release;
int main()
{
Singleton* obja = Singleton::GetInstance(10);
Singleton* objb = Singleton::GetInstance(10);
Singleton* objc = Singleton::GetInstance(10);
Singleton* objd = Singleton::GetInstance(10);
cout << obja <<endl;
cout << objb << endl;
cout << objc << endl;
cout << objd << endl;
return 0;
}
运行结果:
懒汉式的一个细节问题
我们如果将懒汉模式的对象初始化放在GetInstance函数中,在多线程情况下会不会出现多次初始化的情况,如下:
class Singleton
{
private:
int value;
private:
Singleton(int x=0):value(x){}
~Singleton(){cout<<"~Singleton()"<<endl;}
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
public:
static Singleton* GetInstance(int x)
{
static Singleton* pobj;
if (pobj == NULL)
{
pobj = new Singleton(10);
}
return pobj;
}
class Release//定义一个嵌套类
{
public:
~Release()
{
delete pobj;
pobj=NULL;
}
}
static Release release
};
答案是不会的,静态成员变量是在编译时期初始化好的,在主函数执行时,不会在执行初始化语句,所以不用担心线程安全问题。