单例模式是说,一个类不论创建多少次对象,永远只能得到该类的一个对象的实例。
因为静态成员只属于类而不属于任何一个对象,所以可以想象用静态成员是不是可以实现单例。
1.要限制对象的个数,首先要限制构造函数————构造函数私有化
2.定义一个唯一的类的实例对象——static变量,只属于类而不属于任何一个对象,且需要类外初始化。如果类内初始化那就违背了
3.定义一个静态成员函数getInstance()来获取该对象。————因为不能通过构造函数获取对象,所以只能通过静态成员函数获取该唯一的对象
4.不仅要限制构造,还要限制拷贝构造,和赋值重载(因为类的功能很多,得主动去掉,不然会忘记),限制的方式也是C++11新特性,将函数赋值为delete
class Singleton
{
public:
static Singleton* getInstance()
{
return &instance;
}
private:
static Singleton instance; //2.定义一个唯一的类的实例对象
Singleton() //1.构造函数私有化
{
//省略构造函数执行的语句
}
Singleton(const Singleton&) = delete;//C++11中,定义成员函数,可在后面使用 = delete修饰,表示该函数被删除,禁用;
Singleton& operator=(const Singleton&) = delete;
};
Singleton Singleton::instance;//调用无参构造函数,我们省去了构造函数的操作
int main(){
Singleton* p1 = Singleton::getInstance();//再写Singleton* p2 = Singleton::getInstance();或p3等,得到的依然是一模一样的地址
//禁用拷贝构造后下面这个语句就会报错了
Singleton t = *p1;
return 0;
}
饿汉式单例模式:还没有获取实例对象,实例对象就已经产生了
懒汉式单例模式:唯一的实例对象,直到第一次获取它的时候,才产生
前面的例子中,因为对象是static的,所以在函数没有被调用的时候,对象就已经存在且被构造好了(编译期就存在于全局/静态存储区——或者实际上是在操作系统的数据段,没有初始化的就是在数据段其中的.bss段,初始化了的就是在数据段的.data段)
所以上面是饿汉式的单例。饿汉式单例一定是线程安全的,因为多线程就是不同线程都调用函数,而饿汉式单例的对象和函数没有关系,所以一定是线程安全的
但是饿汉式的问题是:不管调用不调用那些函数,用不用这个对象,它都会构造好该对象,而构造函数实际上在业务中可能会做很多事情,又要时间又要内存。
比如一个软件,在启动的时候,还没有用哪个功能呢,就已经在进行初始化了,这可能要卡很久。我们应该在需要使用该对象的功能的时候才实例化它。
因为饿汉式单例模式的缺点,我们应该用懒汉式单例模式
首先将static类型的对象改成指针,调用函数获取该指针时,首先判断指针是否为空,为空就new一个,不为空直接返回指针
我们初始化其为空,这样就需要new一个了,然后第二次调用该函数getInstance()
就不为空了,然后直接返回。
class Singleton
{
public:
static Singleton* getInstance()
{
if(instance == nullptr){
instance = new Singleton();
}
return instance;//不为空则直接返回
}
private:
static Singleton* instance;
Singleton()
{
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Singleton* Singleton::instance = nullptr;//初始为空
int main(){
Singleton* p1 = Singleton::getInstance();
return 0;
}
上述在单线程环境下没问题,因为单线程情况下函数getInstance()
不会被同时调用,只能调用完,再调用
但是在多线程条件下,该函数不是可重入函数。多线程条件下,某个线程调用该函数,没有执行完即对象instance还没被创建,另一个线程也执行,发现instance为空,那也执行创建语句instance = new Singleton();
,所以该函数不是可重入函数
可重入函数是指能够被多个线程“同时”调用的函数,并且能保证函数结果正确不必担心数据错误的函数
使用不可重入的函数就要考虑线程安全了
编译器执行该函数相当于做了三件事:
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
/*
开辟内存
构造对象
给instance赋值
*/
instance = new Singleton();
}
return instance;
}
1)就像前面说的,当线程A还在执行new的时候,没有将new出来的对象赋值给instance,线程B又执行该函数发现instance还是nullptr,所以也执行临界区代码去new一个对象,违背了单例。
2)另外,实际上编译器为了加快代码执行速度,可能先赋值,再构造对象
————也容易理解,比如该对象属性很多,构造比较久,先给该变量赋值为初始值默认值,再对该变量进行构造
开辟内存
给instance赋值
构造对象
所以,线程A执行开辟内存,对instance赋值,线程B执行到这发现instance不为空,直接return insatnce
,返回一个未经构造的对象,那后面访问这个未经构造的对象就会出错了,其属性和一些数据就是错误的
所以从上面两个方面来说,我们都得给临界区instance = new Singleton();
加锁,不能让多个线程同时执行这一句
#include
std::mutex mtx;//全局的锁
class Singleton
{
public:
static Singleton* getInstance()
{
lock_guard<std::mutex> guard(mtx);//C++11新特性,锁管理工具
if(instance == nullptr){
instance = new Singleton();
}
return instance;
}
std::lock_guard属于C++11特性,锁管理遵循RAII习语管理资源。原理是我们把mtx这个互斥锁赋值给lock_guard的对象guard,它会在它自己的构造函数里调用互斥体的lock()函数进行加锁,在析构函数里调用互斥体的unlock()函数进行解锁,封装好加锁解锁过程
guard是警卫,守卫的意思
这里guard对象是局部变量,函数执行完guard就失效,guard就会自己的执行析构函数,这个时候就会解锁——像只能指针
但是这个锁的粒度太大了,单线程环境下每次调用该函数也会执行加锁操作
所以还得修改,改成下面之后,单线程环境下只有第一次会加锁,第二次以后调用就不会加锁了:
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
lock_guard<std::mutex> guard(mtx);//放到if里面
instance = new Singleton();
}
return instance;
}
这个情况下,出这个if的括号就会释放锁了,(return instance前面的括号)。这个也有问题,第二个线程阻塞后还是会执行new,所以需要加一个if判断。
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
lock_guard<std::mutex> guard(mtx);
if(instance == nullptr) //还要判断下是否为空
{
instance = new Singleton();
}
}
return instance;
}
这个instance是static的,存储在全局/静态变量区——操作系统的数据段。是同一个进程,多个线程共享的数据。
CPU在执行线程指令的时候,为了加快多线程的执行速度,会将这些线程共享的数据都拷贝一份到自己的线程缓存thread cache————CPU上的缓存上,我们这里就是instance对象
所以我们要加一个关键字**volatile**
加了之后,当instance这个共享变量改变之后,所有线程看到的都是改变之后的instance,而不是自己缓存上的那份拷贝。也就是线程看到的是内存上那个位置的instance,而不是自己的线程缓存上的,所以volatile的修饰其实就是告诉线程该去原来的地址上找该变量
因为可能自己拷贝的那份没来得及更新修改
#include
using namespace std;
#include
std::mutex mtx;//全局的锁
class Singleton
{
public:
static Singleton* getInstance()
{
if(instance == nullptr)
{
lock_guard<std::mutex> guard(mtx);//C++ 11新特性,所管理工具
if(instance == nullptr) //双重判断
{
instance = new Singleton();
}
}
return instance;
}
private:
static Singleton* volatile instance; //2.定义一个唯一的类的实例对象 //加上volatile
Singleton() //1.构造函数私有化
{
//省略构造函数的具体实现,实际业务中会有很多功能
}
Singleton(const Singleton&) = delete;//C++11中,定义成员函数,可在后面使用 = delete修饰,表示该函数被删除,禁用;
Singleton& operator=(const Singleton&) = delete;
};
Singleton*volatile Singleton::instance = nullptr; //加上volatile
int main(){
Singleton* p1 = Singleton::getInstance();
return 0;
}
就是将该对象放在函数里,成为**静态局部变量**。注意,静态局部变量也是在全局区(C++内存模型的叫法),和静态全局变量,全局变量一样,是在编译器就被分配了内存。
但是静态局部变量的初始化是运行到该语句时,进行初始化
C和C++的处理还不一样
c语言,编译时分配内存和初始化的。
C++,编译时分配内存 ,(运行时)首次使用时初始化。
主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造。
public:
static Singleton* getInstance()
{
static Singleton instance;//静态局部变量。第一次运行到这里的时候就进行初始化
return &instance;
}
也因为是调用该函数才会初始化该对象,没调用的话该对象不会调用构造函数,所以也是一个懒汉式单例模式。
多线程情况下:
所以如果线程A调用该函数,在初始化未完成之前,线程B不会执行该初始化操作。线程A完成初始化后,已经初始化过的变量,其它线程不会再重复进行初始化操作,从而只有一个实例对象产生。
———————————————————关于静态成员变量的初始化
——————————————
上面说的太肤浅了。实际上对于这种自定义类型来说,无论**全局静态变量还是局部静态变量**,都是执行**动态初始化**,也就是都得在代码真正执行时调用其构造函数才能初始化。
而编译时的分配内存其实执行的是0初始化。对于static int a = 2;这种,就是静态初始化;对于 static int a;这种没有初始化的,其实也是执行0初始化,0初始化也会被归入静态初始化。
其实只要区分静态和动态就好了。静态就是不需要运行程序,在运行前就能放好。动态初始化就得执行程序。
所以动态初始化的这些,也得先进行静态初始化中的0初始化——分配内存。
所以,自定义类型的静态变量无论全局还是局部,都是和未初始化的静态变量一起被初始化为0,放入.bss段。代码段中放的就是编译好的程序。
所以.bss段其实一开始存放的都是0。值得一提的是,初始化为0静态变量也会放在.bss段。程序在执行之前,.bss段会自动清0.
.bss段中的自定义类型静态变量在程序运行后执行动态初始化,初始化后也不会移动到.data段,毕竟操作系统已经给他们分配好内存在.bss段了,而可执行程序中也有相应的指针指向.bss中对应的内存。