微信公众号:幼儿园的学霸
单例模式顾名思义,保证一个类仅可以有一个实例化对象,并且提供一个可以访问它的全局接口。实现单例模式必须注意一下几点:
其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
唯一:唯一的实例;任何位置获取的都是那个唯一实例
单例模式分为懒汉和饿汉单例模式
懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化一个对象。在访问量较小,甚至可能不会去访问的情况下,采用懒汉实现,这是以时间换空间。
代码中采用了c++11
的delete.在C++11之前,当我们希望一个类不能被拷贝,就会把构造函数定义为private,但是在C++11里就不需要这样做了,只需要在构造函数后面加上=delete来修饰下就可以了。
#include
//
//有缺陷的懒汉单例模式
//缺陷:线程不安全、内存不安全(内存泄露)
class Singleton {
private:
//构造函数私有
Singleton() {
std::cout << "Constructor called!" << std::endl;
}
Singleton(Singleton &) = delete;//明确拒绝
Singleton &operator=(const Singleton &) = delete;
static Singleton *m_instance_ptr;
public:
~Singleton() {
std::cout << "Destructor called!" << std::endl;
}
static Singleton *get_instance() {
if (m_instance_ptr == nullptr) {
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
void use() const { std::cout << "in use" << std::endl; }
};
//类的静态成员变量需要在类外进行初始化
Singleton *Singleton::m_instance_ptr = nullptr;
int main() {
Singleton *instance = Singleton::get_instance();
Singleton *instance_2 = Singleton::get_instance();
instance->use();
return 0;
//运行结果:
//Constructor called!
//in use
}
从运行结果可以看到,获取了两次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现,但是它存在一些问题:
m_instance_ptr
是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr
还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 解决办法:加锁因此,接下来提供一个改进的,线程安全的、使用智能指针的实现
#include
//
//线程安全/内存安全的懒汉单例模式
//智能指针+双检锁 or 智能指针+call_once
采用智能指针,双检锁
//class Singleton{
//public:
// typedef std::shared_ptr Ptr;
//
// //析构函数定义为public
// ~Singleton(){
// std::cout<<"Destructor called!"< lk(m_mutex);
// if(m_instance_ptr == nullptr){//第二次检查
// m_instance_ptr = std::shared_ptr(new Singleton);
// //由于构造函数是private,因此下面这种写法行不通
// //m_instance_ptr = std::make_shared();
// }
// }
// return m_instance_ptr;
// }
//
// void use() const { std::cout << "in use" << std::endl; }
//
//
//private:
// Singleton(){
// std::cout<<"Constructor called!"<(new Singleton);
}
public:
typedef std::shared_ptr Ptr;
~Singleton() {
std::cout << "Destructor called!" << std::endl;
}
static std::shared_ptr get_instance() {
static std::once_flag s_flag;
//std::call_once(s_flag, createInstance);
std::call_once(s_flag, [] { m_instance_ptr = std::shared_ptr(new Singleton); });
return m_instance_ptr;
}
void use() const { std::cout << "in use" << std::endl; }
private:
static std::shared_ptr m_instance_ptr;
};
//静态成员变量初始化
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
int main() {
Singleton::Ptr instance = Singleton::get_instance();
Singleton::Ptr instance2 = Singleton::get_instance();
instance->use();
return 0;
//两种情况下的运行结果:
//Constructor called!
//in use
//Destructor called!
}
从运行结果可以看到,确实只构造了一次实例,并且发生了析构。
shared_ptr
和mutex
都是C++11
的标准,以上这种方法的优点是:
shared_ptr
, 用了C++
比较倡导的 RAII思想,用对象管理资源,当 shared_ptr
析构的时候,new
出来的对象也会被 delete
掉。以此避免内存泄漏。get_instance
的方法都加锁,锁的开销毕竟还是有点大的。不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。
还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!
观察上面智能指针+双检锁的代码,表面上这段代码是没有问题的,但实际还是存在问题,来源于CPU的乱序执行,c++
中new
分为两步:
1.分配内存
2.调用构造函数
所以 pInst = new T()
;这句代码分为三步:
1.分配内存
2.在内存的位置上调用构造函数
3.将内存的地址复制给pInst
在这三步中,2和3的顺序是可以颠倒的。也就是说,完全有可能出现pInst
的值已经不是nullptr
,但对象仍然没有构造完毕,这时候如果出现另外一个对GetInstance
的并发调用,此时第一个if内的pInst
已经不为nullptr
。所以这个调用会直接返回尚未构造完全的对象的地址以提供给用户使用。那么这个程序就存在崩溃的可能性。
上面代码中的第2中方法:智能指针+
call_once
的模式理论上可以避免上面的问题,查找资料,还没有查找到相关的介绍,因此可以理解为能够避免双检锁失效的问题。
关于上面介绍,可以搜索: C++11修复了双重检查锁定问题
不过,在使用单例模式的时候尽量用最推荐的模式 – 局部静态变量,也就是下面的代码。
这种方法又叫做 Meyers’ SingletonMeyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
#include
//
//采用局部静态变量
//
class Singleton {
public:
~Singleton() {
std::cout << "Destructor called!" << std::endl;
}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static Singleton &get_instance() {
static Singleton instance;
return instance;
}
void use() const { std::cout << "in use" << std::endl; }
private:
Singleton() {
std::cout << "Constructor called!" << std::endl;
}
};
int main(int argc, char *argv[]) {
Singleton &instance_1 = Singleton::get_instance();
Singleton &instance_2 = Singleton::get_instance();
instance_1.use();
return 0;
//运行结果
//Constructor called!
//in use
//Destructor called!
}
C++
静态变量的生存期是从声明到程序结束,这也是一种懒汉式
另外网上有人的实现返回指针而不是返回引用,
static Singleton* get_instance(){
static Singleton instance;
return &instance;
}
这样做并不好,理由主要是无法避免用户使用delete instance
导致对象被提前销毁。还是建议大家使用返回引用的方式。
饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
#include
//
//饿汉单例模式
//
//饿汉式:线程安全,注意一定要在合适的地方去delete它
//.h头文件
class Singleton
{
public:
static Singleton* getInstance();
void use() const { std::cout << "in use" << std::endl; }
~Singleton() {
std::cout << "Destructor called!" << std::endl;
}
private:
Singleton(){//构造函数私有
std::cout << "Constructor called!" << std::endl;}
Singleton(const Singleton&) = delete; //明确拒绝
Singleton& operator=(const Singleton&) = delete; //明确拒绝
static Singleton* m_pSingleton;
};
//.cpp文件
Singleton* Singleton::m_pSingleton = new Singleton();
Singleton* Singleton::getInstance()
{
return m_pSingleton;
}
int main()
{
Singleton* instance = Singleton::getInstance();
instance->use();
delete instance;//需要手动delete
instance = nullptr;
return 0;
//运行结果
//Constructor called!
//in use
//Destructor called!
}
#include
//
//饿汉单例模式
//
//饿汉式:线程安全,同时采用智能指针的方式
//.h头文件
class Singleton {
public:
typedef std::shared_ptr Ptr;
static Ptr getInstance();
void use() const { std::cout << "in use" << std::endl; }
~Singleton() {
std::cout << "Destructor called!" << std::endl;
}
private:
Singleton() {//构造函数私有
std::cout << "Constructor called!" << std::endl;
}
Singleton(const Singleton &) = delete; //明确拒绝
Singleton &operator=(const Singleton &) = delete; //明确拒绝
static Ptr m_pSingleton;
};
//.cpp文件
Singleton::Ptr Singleton::m_pSingleton = std::shared_ptr(new Singleton);
Singleton::Ptr Singleton::getInstance() {
return m_pSingleton;
}
int main() {
Singleton::Ptr instance = Singleton::getInstance();
instance->use();
return 0;
//运行结果
//Constructor called!
//in use
//Destructor called!
}
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承(有资料说登记式可以继承,待确定!)
具体运用场景如:
你需要系统中只有唯一一个实例存在的类的全局变量的时候才使用单例。
1.C++ 常用设计模式(学习笔记)
2.最推荐的懒汉式单例(magic static )——局部静态变量
3.C++ 单例模式总结与剖析
4.单例模式
5.探索单例模式
下面的是我的公众号二维码图片,欢迎关注。