1、定义
保证一个类仅有一个实例,并提供该实例的全局访问点。
2、示例
原始的单例模式
单例模式要做如下事情:
不能通过构造函数构造,否则就能够实例化多个。构造函数需要私有声明
保证只能产生一个实例
下面是一个简单的实现:
class Singleton
{
private:
static Singleton *instance;
Singleton(){};
public:
static Singleton *getInstance()
{
if (instance== nullptr)
{
instance= new Singleton();
}
return instance;
}
};
Singleton * Singleton::instance= nullptr;
int main()
{
Singleton * s = Singleton::getInstance();
return 0;
}
使用局部静态对象来解决存在的两个问题
刚刚的代码中有两个问题,一个是多线程的情况下可能会出现new两次的情况。另外一个是程序退出后没有运行析构函数。
下面采用了静态对象来解决。
在C++11标准中,要求局部静态变量初始化具有线程安全性,推荐使用static局部对象。
另外还有一个版本的懒汉模式代码,也是支持线程安全(打开编译器C++11支持):
class Singleton
{
private:
static Singleton *instance;
Singleton(){cout << "构造" << endl;};
~Singleton(){cout << "析构" << endl;}
public:
static Singleton *getInstance()
{
static Singleton s;
return &s;
}
};
int main()
{
cout << "单例模式访问第一次前" << endl;
Singleton * s = Singleton::getInstance();
cout << "单例模式访问第一次后" << endl;
cout << "单例模式访问第二次前" << endl;
Singleton * s2 = Singleton::getInstance();
cout << "单例模式访问第二次后" << endl;
return 0;
}
该代码可能在c++11之前的版本导致多次构造函数的调用,所以只能在较新的编译器上使用。
所以,c++11之前的版本,静态对象线程会不安全
使用mutex以及静态成员来析构单例。
该方案的劣处在于锁导致速度慢,效率低。但是至少是正确的,也能在c++11之前的版本使用,代码的示例如下:
//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
使用双锁检查导致未初始化的内存访问
使用如下的代码来实现已经初始化的对象的直接返回。可以使上述代码性能会大大加快。
由于CPU乱序执行,可能导致访问到未经初始化的对象的引用,导致未定义行为导致段错误。
https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
假如线程A进入锁内并分配对象的空间,但是由于指令可能乱序,实际上导致instance被先指向一块未被分配的内存,然后再在这块内存上进程初始化。但是在指向后,未初始化前,另一线程B可能通过getInstance获取到这个指针。说白了,实际上m_instance = new Singleton();
做了三件事,第一件事申请一块内存,第二件事调用构造函数,第三件是将该内存地址赋给instance_。但是不同的编译器表现是不一样的。可能先将该内存地址赋给m_instance ,然后再调用构造函数。这是线程A恰好申请完成内存,并且将内存地址赋给m_instance ,但是还没调用构造函数的时候。线程B执行到第一个if,判断m_instance 此时不为空,则返回该变量,然后调用该对象的函数,但是该对象还没有进行构造。
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
乍看这种代码是没有问题的,但是问题的来源是CPU的乱序执行,C++的New操作实际上包含了两个步骤:
1、分配内存
2、调用构造函数
所以准确来说为三个步骤
1、分配内存
2、在内存的位置上调用构造函数
3、将内存的地址赋值给pInst
因为(2)和(3)是可以颠倒的,所以可以出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。如果另外一个线程对GetInstance的调用,此时第一个if为false,这样就会返回一个未构造完成的对象,此时可能会导致程序崩溃。
解决思路
许多体系结构都提供barrier
指令,POWERPC
提供了其中一条名为lwsync
的指令,我们可以这样来保证线程安全:
#define barrier() __asm__ volatile ("lwsyc")
volatile T* pInst = 0;
T* GetInstance()
{
if (!pInst)
{
lock();
if (!pInst)
{
T* temp = new T;
barrier()
pInst = temp;
}
unlock();
}
return pInst;
}
https://blog.csdn.net/bobodem/article/details/82860856
尝试使用局部变量并不能保证指令执行顺序
下述代码是一个想法很好但是无法实现目的代码:因为尝试使用临时变量强制指定指令运行顺序时,仍然会被编译器认为是无用的变量,然后被优化掉。
Singleton* Singleton::getInstance() {
if(m_instance== nullptr)
{
static mutex mtx;
lock_guard<mutex> lock(mtx);
if (m_instance== nullptr)
{
auto tmp = new Singleton()
m_instance= tmp;
}
}
return instance;
}
使用原子操作的内存顺序
class Singleton
{
private:
// static volatile Singleton * volatile local_instance;
static atomic<Singleton*> instance;
Singleton(){
cout << "构造" << endl;
};
~Singleton(){
cout << "析构" << endl;
}
public:
static Singleton *getInstance()
{
Singleton* tmp = instance.load(std::memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if(tmp == nullptr){
static mutex mtx;
lock_guard<mutex> lock(mtx);
tmp = instance.load(memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton();
atomic_thread_fence(memory_order_release);
instance.store(tmp, memory_order_relaxed);
}
}
return tmp;
}
};
atomic<Singleton*> Singleton::instance;
在C++11中提供一种方法,使得函数可以线程安全的只调用一次。即使用std::call_once和std::once_flag。std::call_once是一种lazy load的很简单易用的机制。
#include <iostream>
#include <memory>
#include <mutex>
class Singleton {
public:
static Singleton& GetInstance() {
static std::once_flag s_flag;
std::call_once(s_flag, [&]() {
instance_.reset(new Singleton);
});
return *instance_;
}
~Singleton() = default;
void PrintAddress() const {
std::cout << this << std::endl;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
static std::unique_ptr<Singleton> instance_;
};
std::unique_ptr<Singleton> Singleton::instance_;
int main() {
Singleton& s1 = Singleton::GetInstance();
s1.PrintAddress();
Singleton& s2 = Singleton::GetInstance();
s2.PrintAddress();
return 0;
}
call_once使用模板
template <class T>
class Singleton
{
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
public:
template <typename... Args>
static T* instance(Args&&... args)
{
std::call_once(_flag, [&](){
_instance = new T(std::forward<Args>(args)...);
});
return _instance;
}
static void destroy()
{
if (_instance)
{
delete _instance;
_instance = NULL;
}
}
private:
static T* _instance;
static std::once_flag _flag;
};
template <class T>
T* Singleton<T>::_instance = NULL;
template <class T>
std::once_flag Singleton<T>::_flag;
个人还是推荐使用静态局部变量,代码简单,现在基本所有c++项目都支持c++11。此外,原子操作也值得推荐,因为这确实是从根本上解决了单例的线程安全以及效率问题。
单例的应用
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
return &m_obj;
}
// 任务队列是否为空
bool isEmpty()
{
lock_guard<mutex> locker(m_mutex);
bool flag = m_taskQ.empty();
return flag;
}
// 添加任务
void addTask(int data)
{
lock_guard<mutex> locker(m_mutex);
m_taskQ.push(data);
}
// 取出一个任务
int takeTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
return m_taskQ.front();
}
return -1;
}
// 删除一个任务
bool popTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
m_taskQ.pop();
return true;
}
return false;
}
private:
TaskQueue() = default;
static TaskQueue m_obj;
queue<int> m_taskQ;
mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;
int main()
{
thread t1([]() {
TaskQueue* taskQ = TaskQueue::getInstance();
for (int i = 0; i < 100; ++i)
{
taskQ->addTask(i + 100);
cout << "+++push task: " << i + 100 << ", threadID: "
<< this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
});
thread t2([]() {
TaskQueue* taskQ = TaskQueue::getInstance();
this_thread::sleep_for(chrono::milliseconds(100));
while (!taskQ->isEmpty())
{
int data = taskQ->takeTask();
cout << "---take task: " << data << ", threadID: "
<< this_thread::get_id() << endl;
taskQ->popTask();
this_thread::sleep_for(chrono::seconds(1));
}
});
t1.join();
t2.join();
}