一、动机
1、在软件系统中,经常有这样一个特殊的类,必须保证他们在系统中只存在一个实例,才能保证他们的逻辑正确性,以及良好的效率。
2、如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例;
3、这应该是设计者的责任,而不是使用者的责任;
4、保证一个类仅有一个实例,并提供一个该实例的全局访问点(GOF)
二、实现分析
1、非线程安全的单件模式;
#ifndef SINGLETON_H#define SINGLETON_H#include<iostream>class singleton{private: singleton(int data=0):data(data) { std::cout << "创建构造函数" << std::endl; }; singleton(const singleton&) {}; ~singleton() {}; static singleton* point; int data;public: static singleton* get_singleton(int); void set_singleton(int); void get_data();};singleton* singleton::point= nullptr;singleton* singleton::get_singleton(int data = 0){ if (point == nullptr) { point = new singleton(data); } return point;};void singleton::set_singleton(int data) { this->data = data;}void singleton::get_data() { std::cout << "the data is : " << this->data<<std::endl;}#endif // !SINGLETON_H
我们实现了最简单的单例模式,为了满足仅仅生成一个实例,因此我们应该为客户重新提供一个实例化接口并且屏蔽类的默认构造函数与默认复制构造函数,随后实现这个实例化接口,我们可以看到以下代码句为核心:
singleton* singleton::get_singleton(int data = 0){ if (point == nullptr) { point = new singleton(data); } return point;};
首先判断是否已经实例化了这个单一对象,因为实例化这个单一对象的代价是这个对象的静态成员指针非空,因此可以使用这一项进行检查,这个代码在单线程的情况下可以正确使用。
但是在多线程的情况下,若线程1执行了if (point == nullptr)被挂起,而线程2继续执行则会创建一个对象,当线程1再次被激活后又会再创建一个对象,这就造成了之前线程1创建的内存泄漏的问题。
2、单锁的线程安全的单件模式;
由于1中的单件模式存在的问题,使用加锁可以解决这个问题,代码如下:
#ifndef SINGLETON_H#define SINGLETON_H#include<iostream>#include<thread>#include<mutex>using std::mutex;using std::lock_guard;mutex mut;class singleton{private: singleton(int data=0):data(data) { std::cout << "创建构造函数" << std::endl; }; singleton(const singleton&) {}; ~singleton() {}; static singleton* point; int data;public: static singleton* get_singleton(int); void set_singleton(int); void get_data();};singleton* singleton::point= nullptr;singleton* singleton::get_singleton(int data = 0){ lock_guard<mutex> lock(mut);//上锁与自动解锁 if (point == nullptr) { point = new singleton(data); } return point;};void singleton::set_singleton(int data) { this->data = data;}void singleton::get_data() { std::cout << "the data is : " << this->data<<std::endl;}#endif // !SINGLETON_H
可以看到我们使用了锁来解决这个问题,这的确在逻辑上达到了线程安全,但是这种写法依旧存在一个问题,那就是加锁的代价太高,在下面代码中
singleton* singleton::get_singleton(int data = 0){ lock_guard<mutex> lock(mut);//上锁与自动解锁 if (point == nullptr) { point = new singleton(data); } return point;};
所有的线程都需要进行上锁与解锁操作,即使是point != nullptr。这个很不合理,因为仅仅在point == nullptr时候才需要加锁,考虑到这个原因需要在point != nullptr不进行加锁。
3、双检查锁的线程安全(但是由于内存读写reorder,不能用,不安全);
为了提升效率,在point != nullptr不进行加锁,那么写成如下形式正确吗?
很明显是存在问题的,这样写的话若所有线程都在point == nullptr挂起直接就让锁失效了。
singleton* singleton::get_singleton(int data = 0){ if (point == nullptr) { lock_guard<mutex> lock(mut);//上锁与自动解锁 point = new singleton(data); } return point;};
正确的写法应该为:
singleton* singleton::get_singleton(int data = 0){ if (point == nullptr) { lock_guard<mutex> lock(mut);//上锁与自动解锁 if(point==nullptr) point = new singleton(data); } return point;};
在加锁后进行二次判断point == nullptr,这样就能够达到逻辑上的正确了。
但是依旧存在问题:由于new的操作与底层编译器有关,当底层的编译器按照malloc->构造函数->赋值给point的顺序来执行,但是某些编译器为了优化会采用malloc->赋值给point->构造函数的顺序来执行,这就是reorder的问题,当线程1在完成赋值给point过程却被挂起,线程2开始执行这个代码的时候检查第一个point == nullptr?其会发现point != nullptr,因此其会返回一个未经构造函数构造的对象内存。
4、C++11的跨平台双检查锁的线程安全,使用的是atomic(屏蔽编译器的reorder);
修改后的代码如下:
#ifndef SINGLETON_H#define SINGLETON_H#include<iostream>#include<thread>#include<mutex>#include<atomic>using std::mutex;using std::lock_guard;class singleton{private: singleton(int data=0):data(data) { std::cout << "创建构造函数" << std::endl; }; singleton(const singleton&) {}; ~singleton() {}; static singleton* point; int data;public: static singleton* get_singleton(int); void set_singleton(int); void get_data();};static mutex mut;static std::atomic<singleton*> s_p;singleton* singleton::point= nullptr;singleton* singleton::get_singleton(int data = 0){ singleton* point = s_p.load(std::memory_order_relaxed); std::_Atomic_thread_fence(std::memory_order_acquire); if (point== nullptr) { lock_guard<mutex> lock(mut);//上锁与自动解锁 if(point ==nullptr) point = new singleton(data); std::_Atomic_thread_fence(std::memory_order_release); s_p.store(point, std::memory_order_relaxed); } return point;};void singleton::set_singleton(int data) { this->data = data;}void singleton::get_data() { std::cout << "the data is : " << this->data<<std::endl;}#endif // !SINGLETON_H
当然在JAVA中存在volatile,而c++中没有。
5、懒汉模式与饿汉模式
上述的都是懒汉模式,即要到使用了才创建对象,还有一种饿汉模式,其是在使用该单一对象前就创建了这个对象,如下所示:
class singleton{protected: singleton() {};private: static singleton* p;public: static singleton* initance();};singleton* singleton::p = new singleton();singleton* singleton::initance(){ return p;}
这个是绝对的线程安全的,实际上对于开发者而言饿汉模式恰巧是最懒的,但是存在的问题是,如果这个单一类需要入口参数,那该怎么办?
三、要点总结
1、Singleton模式中的实例构造器可以设置为protected以允许子类派生;
2、Singleton模式一般不要支持拷贝构造函数和Colne接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背;
3、如何实现多线程下的Siglenton?注意对双检查锁的正确实现;
4、懒汉模式与饿汉模式的区别。