dylanin1999

导航

设计模式之三:单例模式 Singleton

在实际工作中,我们很多时候在对类对象的管理和使用上,希望自己定义的类只有一个实例化对象,这样才能保证,在整个流程中使用的都是同一个实例对象,从而保证流程的正确性,那么,我们要怎么样才能,让自己定义的对象只被声明一次呢?

我们不能简单地说,那我告诉,类的使用者,让他只实例化一次不就好了吗?

但是,我们要说,在一套好的代码里,这是类定义者该做的事情,而不是类的使用者的责任

下面,我们来介绍,单例模式

单例模式:

动机:

     1、在软件系统中,经常有这样的一些特殊的类,必须保证他们在系统中只存在一个实例,这样才能保证他们的逻辑正确性,以及良好的效率 

      2、如何绕开常规的构造器,提供一种机制来保证一个类只有一个实例?(我们不能单单说,我们(类设计者)告诉用户(类使用者),你就只能实例化一个对象,不要弄多了)

      3、这应该是类设计者的责任,而不是使用者的责任
      

模式定义:        保证一个类仅有一个实例,并提供一个该实例的全局访问点

实现版本:
        1、懒汉模式:顾名思义,非常“懒”,懒汉模式不会迫切需要单例模式,只有在第一次需要该单例实例对象的时候,才会生成该单例实例对象,之后再获得该单例实例时,该单例实例就是第一次获取时所产生的并返回的静态变量。但是,懒汉模式的问题在于,会导致在多线程的情况下,代码变得不安全
                ①、单线程版本   ——在多线程下是不安全的
                ②、多线程版本   ——多线程单独加锁模式,有效简单但消耗大
                ③、多线程双检查锁优化版本   ——在多线程下的双检查模式,有效的优化了加锁的时机。
                    在除初次生成实例外,此后的所有调用获取单例都不需要加锁,但是会由于指令层面的reorder
                    导致,双检查锁的失效。
                ④、C++11版本之后的跨版本实现(volatile) ——在这种方法下,代码在指令层面不会被reorder,
                    这样就解决了双检查锁失效的问题(volatile确保代码顺序在指令层面不会被reorder)


        2、饿汉模式:特点,空间换时间,在单例类定义的时候就进行实例化,饿汉模式的优点在于, 他是线程安全的。

 

下面我们来看一下实现代码:

1、线程非安全版本 (普通懒汉模式)

class Singleton
{
private:
	Singleton();
	Singleton(const Singleton& other);
public:
	static Singleton* getInstance();
	static Singleton* m_instance;
};

Singleton* Singleton::getInstance()
{
	if (m_instance==nullptr)
	{
		m_instance = new Singleton();
	}
	return m_instance;
}

2、线程安全版本,但锁的代价过高 (线程安全的懒汉模式)

class Singleton
{
private:
	Singleton();
	Singleton(const Singleton& other);
public:
	static Singleton* getInstance();
	static Singleton* m_instance;
};


Singleton* Singleton::getInstance()
{
	std::mutex lock;
	lock.lock();
	if (m_instance == nullptr)
	{
		m_instance = new Singleton();
	}
	lock.unlock();
	return m_instance;
}

3、双检查锁,但是由于内存的读写reorder不安全 (双检查锁的懒汉模式)

class Singleton
{
private:
	Singleton();
	Singleton(const Singleton& other);
public:
	static Singleton* getInstance();
	static Singleton* m_instance;
};

/*reorder 问题:再指令层面,代码的调用顺序并不一定是按照我们代码编写的顺序执行,这样就有可能导致
双检查锁的失效*/
/*我们假定的顺序 :先分配内存,然后执行构造器,然后将地址赋值给m_instance
可能的顺序:先分配内存,然后将地址赋值给m_instance,然后执行构造器  (优化结果)
这样导致的结果就是:在地址被复制给m_instance后,m_instance!=nullptr了,
但是此时,constructor还没有被执行*/

Singleton* Singleton::getInstance() 
{
	if (m_instance==nullptr)
	{
		std::mutex lock;
		lock.lock();
		if (m_instance == nullptr)
		{
			m_instance = new Singleton();
		}
		lock.unlock();
 	}
	return m_instance;
} 

4、C++11版本之后的跨版本实现(volatile)

class SingletonCPP
{
private:
	SingletonCPP();
	SingletonCPP(const SingletonCPP& other);
public:
	static SingletonCPP* getInstance();
	static std::atomic<SingletonCPP*> m_instance;
	static std::mutex m_mutex;
};


SingletonCPP* SingletonCPP::getInstance()
{
	SingletonCPP* 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 SingletonCPP;
			std::_Atomic_thread_fence(std::memory_order_release);//释放内存fence
			m_instance.store(tmp, std::memory_order_relaxed);
		}
	}
	return tmp;
}

5、饿汉模式

class Singleton
{
public:
	static Singleton* GetInstance();

private:
	static Singleton* m_instance;
	Singleton();
};

Singleton* Singleton::m_instance = new Singleton;
Singleton* Singleton::GetInstance()
{
	return m_instance;
}

 

要点总结:
        1、Singleton模式中的实例构造器可以设置为protected以允许子类派生
        2、Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能导致多个对象实例,
           与Singleton模式的初衷违背
        3、如何实现多线程环境下安全的Singleton? 注意对双检查锁的正确实现

更多有关设计模式的实例代码可以看我的GitHubhttps://github.com/Dylanin1999/Design-Pattern

posted on 2022-08-13 16:15  DylanYeung  阅读(19)  评论(0编辑  收藏  举报