「生活可以更简单, 欢迎来到我的开源世界」
  1. 懒汉式单例
  2. 懒汉+互斥变量:
  3. 懒汉+检测后加锁——DCL(双检测锁)
  4. call_once/once_flag
  5. Meyers’ Singleton
  6. static local 单例模板
  7. 参考文章
单例模式
2021-03-03

使用C++实现单例模式样例

单例模式应该返回引用,而不是返回指针,避免滥用。

懒汉式单例

#include <iostream>
#include <thread>
#include <chrono>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

class Single{
public:
static Single &instance(){
if(ptr_single == nullptr){
std::this_thread::sleep_for(std::chrono::milliseconds(1000));//睡眠1000毫秒
ptr_single = new Single();
cout << "init" << endl;
}

cout << "not init" << endl;
return *ptr_single;
}
private:
static Single* ptr_single;//静态单例指针,保存单例对象
Single(const Single &)=delete;
Single()=default;//私有构建函数,只能由编译器调用
};

Single* Single::ptr_single=nullptr;

int main(int argc, char* argv[]){
thread t1(Single::instance);
thread t2(Single::instance);
thread t3(Single::instance);
t1.join();
t2.join();
t3.join();
return 0;
}

存在问题:内存无释放、线程不安全

懒汉+互斥变量:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

std::mutex g_mutex;

class Single{
public:
static Single &instance(){
g_mutex.lock();
if(ptr_single == nullptr){
std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠1000毫秒
ptr_single = new Single();
cout << "init" << endl;
}
else cout << "not init" << endl;
g_mutex.unlock();
return *ptr_single;
}
private:
static Single* ptr_single;//静态单例指针,保存单例对象
Single(const Single &)=delete;
Single()=default;//私有构建函数,只能由编译器调用
};

Single* Single::ptr_single=nullptr;

int main(int argc, char* argv[]){
thread t1(Single::instance);
thread t2(Single::instance);
thread t3(Single::instance);
t1.join();
t2.join();
t3.join();
return 0;
}

存在问题:不管需不需要初始化对象,都有加锁和解锁。

懒汉+检测后加锁——DCL(双检测锁)

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

std::mutex g_mutex;

class Single{
public:
static Single &instance(){
if(ptr_single == nullptr){
g_mutex.lock();
if(ptr_single == nullptr){
std::this_thread::sleep_for(std::chrono::milliseconds(100));//睡眠1000毫秒
ptr_single = new Single();
cout << "init" << endl;
}
else cout << "not init" << endl;
g_mutex.unlock();
}
else cout << "not init" << endl;
return *ptr_single;
}
private:
static Single* ptr_single;//静态单例指针,保存单例对象
Single(const Single &)=delete;
Single()=default;//私有构建函数,只能由编译器调用
};

Single* Single::ptr_single=nullptr;

int main(int argc, char* argv[]){
thread t1(Single::instance);
thread t2(Single::instance);
thread t3(Single::instance);
t1.join();
t2.join();
t3.join();
return 0;
}

存在问题:由于内存读写的乱序执行(编译器的问题),DCL不能保证线程安全。

new操作可分三步骤:

  1. 分配类对象所需的内存
  2. 在分配的内存初始化对象
  3. 把分配的内存的地址赋给指针

该操作只能保证步骤1优先执行,步骤2和步骤3的执行顺序不一定。

针对此问题,Java和C#添加了关键字volatile,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;
}

call_once/once_flag

c++ 11提供std::call_once方法来保证函数在多线程环境中只被调用一次,需要一个once_flag的参数。

类似方法在不同平台有各自的实现:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

std::mutex g_mutex;

class Single{
public:
static Single &get_instance(){
std::call_once(flag, instance);
return *ptr_single;
}
private:
static Single* ptr_single;//静态单例指针,保存单例对象
static std::once_flag flag;
Single(const Single &)=delete;
Single()=default;//私有构建函数,只能由编译器调用
static void instance(){
ptr_single = new Single();
cout << "init" << endl;
}
};

Single* Single::ptr_single=nullptr;
std::once_flag Single::flag;

int main(int argc, char* argv[]){
thread t1(Single::get_instance);
thread t2(Single::get_instance);
thread t3(Single::get_instance);
t1.join();
t2.join();
t3.join();
return 0;
}

Meyers’ Singleton

Meyers’ Singleton,著名的写出《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 <iostream>
#include <thread>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

class Single{
public:
static Single &instance(){
static Single single;
return single;
}
private:
Single(const Single &)=delete;
Single(){//私有构建函数,只能由编译器调用
cout << "init" << endl;
}
};

int main(int argc, char* argv[]){
thread t1(Single::instance);
thread t2(Single::instance);
thread t3(Single::instance);
t1.join();
t2.join();
t3.join();
return 0;
}

static local 的变量只有在第一次执行的时候才会初始化,所以这是延迟初始化。

可以通过静态变量,接收外部条件延迟初始化单例。

#include <iostream>
#include <thread>

using std::cin;
using std::cout;
using std::endl;
using std::thread;

class Single{
public:
static Single &instance(){
static Single single(pram);
return single;
}
static int pram;
private:
Single(const Single &)=delete;
Single(int arg){//私有构建函数,只能由编译器调用
cout << "init: " << arg << endl;
}
};

int Single::pram = 2;

int main(int argc, char* argv[]){
thread t1(Single::instance);
thread t2(Single::instance);
thread t3(Single::instance);
t1.join();
t2.join();
t3.join();
return 0;
}

static local 单例模板

template<typename T>
class Singleton
{
public:
static T& getInstance() {
static T value_; //静态局部变量
return value_;
}

private:
Singleton();
~Singleton();
Singleton(const Singleton&); //拷贝构造函数
Singleton& operator=(const Singleton&); // =运算符重载
};

参考文章

<⇧>