单例模式
文章目录
基本知识
单例模式的学习在多次中遇到,包括但不限于:侯捷:明确拒绝编译器合成构造函数
muduo:Singleton类
flydragon:Config类
因此在这里做一个总结,该如何去用现代C++写一个线程安全的类。
定义:一个类有且仅有一个对象,这种类的设计模式称为单例模式或者单件模式。
用途举例:整个工程读取配置文件的对象,日志对象等。
定义级需求:能够实现只有一个对象。
实现方法:将构造函数,拷贝构造函数,拷贝赋值函数定义为 private; 这样可以避免编译器为我们合成构造函数,也可以避免外界的调用。在C++11中还提供了delete机制可以解决这个问题。
如下:
class Singleton
{
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
};
//c++11的delete机制,要采取这种做法更现代化 但是delete机制之后没法new了
class Singleton
{
public://delete机制下要放到public
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
既然我们已经无法通过调用构造函数来生成对象,那么我们该如何去使用这个类呢以及它的独有对象呢?我们采用public & static 方法,定义类的静态成员变量和静态成员函数,用户可以直接通过类名调用静态成员。
//class中定义
public:
static Singleton* getInstance()
{
if (instance_ == NULL)
instance_ = new Singleton();
return instance_;
}
//用户代码中
Singleton* ct1 = Singleton::getInstance();
这样就实现了要求的只能构造一个对象的最简单的饿汉单例
但是这个版本是很明显存在着线程安全问题的:当线程A执行到instance_ == NULL,而还未执行new的时候,线程B也正好执行到instance_ == NULL,这样线程B也就获得了 执行new的机会,因此的话,Singleton就会被初始化两次。
class Singleton
{
private:
Singleton() { std::cout << "构造单例对象" << std::endl; };
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton *instance_; // 单例对象在这里!
public:
static Singleton* getInstance()
{
if (instance_ == NULL)
instance_ = new Singleton();
return instance_;
}
};
因为现在用了 new 需要delete释放内存,而这很容易造成内存泄露,因此我们肯定是拒绝这种写法,因此考虑采用shared_ptr智能指针。但是shared_ptr只能用于懒汉模式
那么我先对比一下看看懒汉模式:
饿汉模式,就是在类定义的时候就实例化了(因为饿,主观能动性强 - . -)。
Singleton* Singleton:: instance = new Singleton();
是线程安全的,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现(不用锁机制,开销小),可以实现更好的性能。
全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。
5 class Singleton{
6 private:
7 Singleton(){
8 cout<<"i am single"<<endl;
9 }
10
11 static Singleton* instance;
12 public:
13 static Singleton* getInstance(){
14 return instance;
15 }
16 };
17
18 Singleton* Singleton:: instance = new Singleton();
19
20 int main(){
21 Singleton* one = Singleton::getInstance();
22 Singleton* two = Singleton::getInstance();
23 if(one == two){
24 cout<<"确实是单例!"<<endl;
25 }
26 }
饿汉单例的实现
实现的需求:线程安全 且 无内存泄露
1. 是一个"懒汉"单例模式,按需内存分配 也就是 需要用到时才分配对象
2. 基于模板实现,具有很强的通用性。
3. 自动内存析构,不存在内存泄露问题(使用std::shared_ptr)。
4. 在多线程情况下,是线程安全的。
5. 尽可能的高效。(线程安全必定涉及到线程同步,线程同步分为内核级别和用户级别的同步对象,用户级别效率远高于内核级别的同步对象,而用户级别效率最高的是 InterlockedXXXX系列API)。
因此产生了多个版本
版本一:双检查锁,由于内存读写的乱序导致不安全
上面的做法是不管三七二十一,某个线程要访问的时候,先锁上再说,这样会导致不必要的锁的消耗,那么,是否可以先判断下if (m_instance == nullptr)
呢,如果满足,说明根本不需要锁啊!这就是所谓的**双检查锁(DCL)**的思想,DCL即double-checked locking。
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
//先判断是不是初始化了,如果初始化过,就再也不会使用锁了
if(m_instance==nullptr){
Lock lock; //伪代码
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
这样看起来很棒!只有在第一次必要的时候才会使用锁,之后就和实现一
中一样了。
在相当长的一段时间,迷惑了很多人,在2000
年的时候才被人发现漏洞,而且在每种语言上都发现了。原因是内存读写的乱序执行(编译器的问题)。
分析:m_instance = new Singleton()
这句话可以分成三个步骤来执行:
- 分配了一个
Singleton
类型对象所需要的内存。 - 在分配的内存处构造
Singleton
类型的对象。 - 把分配的内存的地址赋给指针
m_instance
。
可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1
是最先执行的,步骤2
,3
却不一定。问题就出现在这。假如某个线程A在调用执行m_instance = new Singleton()
的时候是按照1,3,2
的顺序的,那么刚刚执行完步骤3
给Singleton
类型分配了内存(此时m_instance
就不是nullptr
了)就切换到了线程B
,由于m_instance
已经不是nullptr
了,所以线程B
会直接执行return m_instance
得到一个对象,而这个对象并没有真正的被构造!!严重bug就这么发生了。
版本二:C++11的跨平台实现
java
和c#
发现这个问题后,就加了一个关键字volatile
,在声明m_instance
变量的时候,要加上volatile
修饰,编译器看到之后,就知道这个地方不能够reorder(一定要先分配内存,在执行构造器,都完成之后再赋值)。
而对于c++
标准却一直没有改正,所以VC++
在2005
版本也加入了这个关键字,但是这并不能够跨平台(只支持微软平台)。
而到了c++ 11
版本,终于有了这样的机制帮助我们实现跨平台的方案。
//C++ 11版本之后的跨平台实现
// atomic c++11中提供的原子操作
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
/*
* std::atomic_thread_fence(std::memory_order_acquire);
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
版本三:pthread_once函数实现
在linux中,pthread_once()
函数可以保证某个函数只执行一次
声明: int pthread_once(pthread_once_t once_control, void (init_routine) (void)); 功能: 本函数使用初值为PTHREAD_ONCE_INIT的once_control 变量保证init_routine()函数在本进程执行序列中仅执行一次。
class Singleton{
public:
static Singleton* getInstance(){
// init函数只会执行一次
pthread_once(&ponce_, &Singleton::init);
return m_instance;
}
private:
Singleton(); //私有构造函数,不允许使用者自己生成对象
Singleton(const Singleton& other);
//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
static void init() {
m_instance = new Singleton();
}
static pthread_once_t ponce_;
static Singleton* m_instance; //静态成员变量
};
pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
Singleton* Singleton::m_instance=nullptr; //静态成员需要先初始化
版本四:C++11的最简洁跨平台实现
在C++memory model中对static local variable,说道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.
局部静态变量不仅只会初始化一次,而且初始化还是线程安全的,在c++11版本中,如果一个线程正在初始化一个局部静态变量,而另一个线程也试图初始化这个变量,后面的线程会阻塞,而且静态成员变量会在程序结束时自动释放,不需要考虑内存问题。
class Singleton{
public:
// 注意返回的是引用。
static Singleton& getInstance(){
static Singleton m_instance; //局部静态变量
return m_instance;
}
private:
Singleton(); //私有构造函数,不允许使用者自己生成对象
Singleton(const Singleton& other);
};
这种单例被称为Meyers' Singleton
。这种方法很简洁,也很完美,但是注意:
- gcc 4.0之后的编译器支持这种写法。
- C++11及以后的版本(如C++14)的多线程下,正确。
- C++11之前不能这么写。
但是现在都18年了。。新项目一般都支持了c++11
了。
用模板包装单例
从上面已经知道了单例模式的各种实现方式。但是有没有感到一点不和谐的地方?如果我class A
需要做成单例,需要这么改造class A
,如果class B
也需要做成单例,还是需要这样改造一番,是不是有点重复劳动的感觉?利用c++
的模板语法可以避免这样的重复劳动。
template<typename T>
class Singleton
{
public:
static T& getInstance() {
static T value_; //静态局部变量
return value_;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&); //拷贝构造函数
Singleton& operator=(const Singleton&); // =运算符重载
};
假如有A
,B
两个类,用Singleton
类可以很容易的把他们也包装成单例。
class A{
public:
A(){
a = 1;
}
void func(){
cout << "A.a = " << a << endl;
}
private:
int a;
};
class B{
public:
B(){
b = 2;
}
void func(){
cout << "B.b = " << b << endl;
}
private:
int b;
};
// 使用demo
int main()
{
Singleton<A>::getInstance().func();
Singleton<B>::getInstance().func();
return 0;
}
饿汉与懒汉的比较
比较:
饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的推荐使用第一种 。
从实现方式来讲他们最大的区别就是懒汉式是延时加载,他是在需要的时候才创建对象,而饿汉式在虚拟机启动的时候就会创建,饿汉式无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例(上面有个朋友写错了)、所以如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。
懒汉式的优点是延时加载、缺点是应该用同步(想改进的话现在还是不可能,比如double-check)、其实也可以不用同步、看你的需求了,多创建一两个无引用的废对象其实也没什么大不了。