今天来谈谈单例模式。单例模式是使用最广泛的,也“可能”是最简单的设计模式之一。这里我的“可能”打了引号,意在提醒大家,单例模式虽然代码不多,但是蕴含的理论其实并不少。
首先,单例模式的出现,我个人理解是OOP思想对static变量的面向对象实现。大家知道,如果想要表示一个全局唯一的变量,不随对象的不同而出现新的拷贝,一般语言中都有static关键字。当一个变量被static修饰时,则表示该变量是所有该类的对象共享的。但是,如果我们有很多变量有类似的需求,难道要每个变量都用static修饰吗?显然,单例模式提供了更好的实现方法。
当一个类使用单例模式时,则该类的对象在整个程序运行期间都只有一个对象存在,该对象包含的所有属性都是全局唯一的。因此,通过单例模式也可以实现简单的程序间通信的目的。
要使一个类只存在一个对象,一般的方法如下:
- 首先将构造函数声明为private,这样外部就无法创建该类的对象;
- 提供一个GetInstance()方法或者Instance属性,用于获取唯一的对象。
要实现上面的步骤,方法不是惟一的,这也就导致单例模式有不同的实现:饿汉式和懒汉式单例模式。
饿汉式
饿汉式,顾名思义,非常饥饿,因此迫不及待的创建出唯一的对象,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。
class HungrySingleton {
private:
HungrySingleton() {}
HungrySingleton(const HungrySingleton& a) {}
static HungrySingleton* m_instance;
public:
static HungrySingleton* getInstance() {
return m_instance;
}
};
HungrySingleton* HungrySingleton::m_instance = new HungrySingleton();
懒汉式
懒汉式,顾名思义,太懒了,因此只有等到要使用了才创建惟一的对象。有线程安全和线程不安全两种写法。
懒汉式有必要一步一步深究下细节,首先,我们来实现一个最简单的版本,这也是我们最容易想到的版本。如下:首先声明一个静态对象,并初始化为NULL;然后在调用getInstance()方法时判断如果m_instance为NULL,就创建对象。
class LazySingleton {
private:
static LazySingleton* m_instance;
LazySingleton() {}
LazySingleton(const LazySingleton& a) {}
public:
static LazySingleton* getInstance();
};
LazySingleton* LazySingleton::m_instance = NULL;
//线程非安全版本
LazySingleton* LazySingleton::getInstance() {
if (m_instance == NULL) {
//如果线程1这里时间片到了,交出CPU,会发生多个线程进到这里创建多个对象
m_instance = new LazySingleton();
}
return m_instance;
}
仔细思考就会发现上面的实现存在一定的问题。在多线程的情况下,线程是通过CPU基于时间片轮转算法进行调度的,任意时刻都可能线程的时间片用完,从而交出CPU供其他线程使用。因此,假如线程1判断(m_instance == NULL)后进入if语句,但是在执行m_instance = new LazySingleton();之前时间片到了,交出CPU,从而导致m_instance依然为NULL,导致多个线程进入if语句块,从而导致m_instance被多次new操作,发生错误。如下:我们可以加一个Lock操作,保证getInstance()的原子性。
/*
线程安全版本,但是锁的代价过高(m_instance创建出来之后,
其实不再需要锁了,但是lock仍然存在要加锁操作)
*/
static pthread_mutex_t mutex; //类增加一个属性
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;
LazySingleton* LazySingleton::getInstance() {
pthread_mutex_lock(&mutex);//方法结束lock释放
if (m_instance == NULL) {
m_instance = new LazySingleton();
}
pthread_mutex_unlock(&mutex);
return m_instance;
}
上面的代码任然有不足,每次调用getInstance()都会对其进行加锁操作,锁的代价是很高的。但实际上,只要m_instance被创建后,加锁操作就可以不用了。因此,我们可以用一个双重判空实现只加一次锁。
//双检查锁版本,避免m_instance创建出来之后,仍然加锁,
//但是由于内存读写reorder不安全
LazySingleton* LazySingleton::getInstance() {
if (m_instance == NULL) {
pthread_mutex_lock(&mutex);//方法结束lock释放
if (m_instance == NULL) {//必须再次判空,不然lock没有意义
/*
这条语句CPU执行时有三个过程:1、开辟一段内存2、执行构造函数
3、将引用指向该内存;如果2执行结束后时间片到了,此时m_instance不是空了,
以后所有线程都是直接return不完成的m_instance,只是一段空内存,
里面没有数据
*/
m_instance = new LazySingleton();
}
pthread_mutex_unlock(&mutex);
}
return m_instance;
}
上面的版本在很长一段时间内被公认是良好的解决方案,但是后来的大神们发现仍然存在一个Bug。CPU执行m_instance = new LazySingleton();这段代码时,实际上有三个无序过程(汇编的知识了):1、开辟一段内存2、执行构造函数3、将引用指向该内存。在上述执行过程中仍然可能发生CPU时间片到了的情况,此时m_instance已经不为空了,但是对象数据却是不对的,但是判空条件不满足就会直接返回了。由此,volatile关键字应运而生。
//volatile关键字专为解决该问题
class Singleton
{
private:
static volatile Singleton * volatile local_instance;
Singleton() {
cout << "构造" << endl;
};
~Singleton() {
cout << "析构" << endl;
}
public:
static volatile Singleton *getInstance()
{
if (local_instance == nullptr) {
static mutex mtx;
lock_guard<mutex> lock(mtx);
if (local_instance == nullptr)
{
auto tmp = new Singleton();
local_instance = tmp;
}
}
return local_instance;
}
};
volatile Singleton * volatile Singleton::local_instance = nullptr;
在新的标准中,atomic类实现了内存栅栏,使得多个核心访问内存时可控。这利用了c++11的内存访问顺序可控。
//atomic新标准版本
atomic<LazySingleton*> LazySingleton::m_instance;
LazySingleton* LazySingleton::getInstance() {
LazySingleton* tmp = m_instance.load(memory_order_relaxed);
//获取内存fence,防止CPU进行上诉三条指令的reorder
atomic_thread_fence(memory_order_acquire);
if (tmp == NULL) {
mutex m_mutex;
lock_guard<mutex> lock(m_mutex);//加锁
tmp = m_instance.load(memory_order_relaxed);
if (tmp == NULL) {
tmp = new LazySingleton();
atomic_thread_fence(memory_order_relaxed);//释放内存fence
m_instance.store(tmp, memory_order_relaxed);
}
}
return tmp;
}