正确写法
C++11
首先来看C++11的正确写法:
#include <mutex>
std::mutex mtx;
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 判断一
lock_guard<std::mutex> guard(mtx); // 互斥锁 + 双重判断
if (instance == nullptr) { // 判断二
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() {} // 构造函数私有化
static Singleton *volatile instance; // 定义唯一的类实例对象
Singleton (const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
Singleton *volatile Singleton::instance = nullptr;
C++98
然后是C++98的正确写法:
class Singleton {
public:
static Singleton* getInstance() {
if (item == nullptr) { // 判断一
pthread_mutex_lock(&mutex); // 互斥锁+双重判断
if(item == nullptr) { // 判断二
item = new Singleton();
}
pthread_mutex_unlock(&mutex);
}
return item;
}
static void destoryItem() {
if (item != NULL) {
delete item;
item = NULL;
}
}
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton& s) {};
Singleton &operator = (const Singleton& s);
static pthread_mutex_t mutex;
static Singleton *volatile item;
};
Singleton *volatile Singleton::item = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
原理分析
单线程版本
此时不考虑多线程并发的执行状态,getInstance()函数是不可重入函数。
可重入函数定义:不考虑递归,一个函数在执行完之前能不能再次被调用,比如线程1还未运行完,线程2又执行同一个函数,如果该函数不会发生竞态条件,那么该函数就是可重入函数。
实现:
class Singleton {
public:
static Singleton* getInstance() { // 获取类的唯一实例对象的接口方法
if (instance == nullptr) {
instance = new Singleton(); // 实例化
}
return instance;
}
private:
static Singleton* instance; // 定义一个唯一的类的实例对象
Singleton() {} // 构造函数私有化
Singleton (const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
Singleton* Singleton::instance = nullptr;
当前实现不支持多线程环境运行,具体问题分析如下。
问题
if语句中的类实例化代码在创建对象实例的时候,主要完成三项工作,开辟内存、构造对象、给instance赋值。编译器为了加快代码的运行速度,会对后两步的代码进行顺序倒换,即存在两种执行顺序。接下来对两种顺序中下的问题进行分析。
顺序一
1. 开辟内存 2. 构造对象 3. 给instance赋值
假设程序启动后,线程一先进入if语句,进行了内存开辟、构造对象,此时还未给instance赋值,线程二也开始了运行。线程二在进行if语句判断时,由于线程一没有给instance赋值,instance仍然为空,if语句中的判断为真,线程二再一次进行对象实例化。综上,程序共进行了两次对象的实例化,发生错误。
顺序二
1. 开辟内存 2. 给instance赋值 3.构造对象
假设程序启动后,线程一先进入if语句,进行了内存开辟、给instance赋值,此时还未构造对象,线程二也开始了运行。线程二在进行if语句判断时,此时instance已经被赋值,所以if语句中的判断为假,线程二不进入if语句直接返回instance。但是,instance并没有完成构造,所以线程二返回了无效的实例,发生错误。
考虑线程安全(加锁)
注意,此时仍不完善,只是分析问题的中间步骤:
#include <mutex>
std::mutex mtx;
class Singleton {
public:
static Singleton* getInstance() { // 获取类的唯一实例对象的接口方法
// 问题
lock_guard<std::mutex> guard(mtx); // 互斥锁
if (instance == nullptr) { // 临界区代码
instance = new Singleton();
}
return instance;
}
private:
static Singleton* instance; // 定义一个唯一的类的实例对象
Singleton() {} // 构造函数私有化
Singleton (const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
Singleton* Singleton::instance = nullptr;
此时简单加锁存在问题,位置如代码块中的注释,现在来分析问题。
问题
互斥锁的粒度太大,代码运行时的性能下降。
当线程调用getInstance()函数获取实例对象时,无论当前实例是否存在,进入函数后均需要上锁。特别是当单线程运行时,频繁的加锁解锁会大大降低函数运行时的效率。
考虑线程安全(减小锁的粒度)
注意,此时仍不完善,只是分析问题的中间步骤:
#include <mutex>
std::mutex mtx;
class Singleton {
public:
static Singleton* getInstance() { // 获取类的唯一实例对象的接口方法
if (instance == nullptr) {
lock_guard<std::mutex> guard(mtx); // 互斥锁
// 问题
instance = new Singleton(); // 临界区代码
}
return instance;
}
private:
static Singleton* instance; // 定义一个唯一的类的实例对象
Singleton() {} // 构造函数私有化
Singleton (const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
Singleton* Singleton::instance = nullptr;
此时代码仍然存在问题,位置如代码块中的注释,现在来分析问题。
问题
假设当前的实例化执行顺序为1. 开辟内存 2. 构造对象 3.给instance赋值。
程序启动,线程一开始执行getInstance()函数,if语句判断为真,线程一获得互斥锁,开始进行对象实例化。此时线程二也开始执行,由于此时线程一未完成实例化,线程二执行的if语句同样判断为真,但会在互斥锁的位置阻塞等待。
接下来线程一完成实例化,退出临界区,释放互斥锁,并返回实例化后的对象。此时线程二获取互斥锁,再次进行实例化。由于完成了两次实例化,程序发生错误,如下图。
线程安全版本(锁+双重判断)
于是,双重检查锁,或者说锁+双重判断应运而生,实现方式如下。
#include <mutex>
std::mutex mtx;
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 判断一
lock_guard<std::mutex> guard(mtx); // 互斥锁 + 双重判断
if (instance == nullptr) { // 判断二
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() {} // 构造函数私有化
static Singleton *volatile instance; // 定义唯一的类实例对象
Singleton (const Singleton&) = delete;
Singleton& operator = (const Singleton&) = delete;
};
Singleton *volatile Singleton::instance = nullptr;
锁+双重判断可以克服线程安全问题,getInstance()函数成为可重入函数。
总结
以上就是用C++介绍的单例模式中双重检查锁的全部内容了,如果有问题欢迎评论区讨论,如果有错误也欢迎大家批评指正。