八、单例模式

       今天来谈谈单例模式。单例模式是使用最广泛的,也“可能”是最简单的设计模式之一。这里我的“可能”打了引号,意在提醒大家,单例模式虽然代码不多,但是蕴含的理论其实并不少。
       首先,单例模式的出现,我个人理解是OOP思想对static变量的面向对象实现。大家知道,如果想要表示一个全局唯一的变量,不随对象的不同而出现新的拷贝,一般语言中都有static关键字。当一个变量被static修饰时,则表示该变量是所有该类的对象共享的。但是,如果我们有很多变量有类似的需求,难道要每个变量都用static修饰吗?显然,单例模式提供了更好的实现方法。
       当一个类使用单例模式时,则该类的对象在整个程序运行期间都只有一个对象存在,该对象包含的所有属性都是全局唯一的。因此,通过单例模式也可以实现简单的程序间通信的目的。
       要使一个类只存在一个对象,一般的方法如下:

  1. 首先将构造函数声明为private,这样外部就无法创建该类的对象;
  2. 提供一个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;
}

使用智能指针改进单例模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值