《设计模式》学习笔记——单例模式

单例模式是一种用来创建独一无二的对象的设计模式,以满足相应的应用场景,如系统日志、对话框、设备驱动等对象,这些类仅能有一个实例。单例模式的主要实现方式有两种,分别为饿汉式和懒汉式,两种方法都是将类的构造函数设为私有,禁止在外部构造,并提供一个全局的访问方法让程序的其它模块共享。本文主要对两种单例模式进行简单介绍,并提醒读者双重检查锁机制存在的一些问题及解决方法。

饿汉式(Eager Initialized

饿汉式即提前初始化,该方法在程序运行之初就对所需资源进行初始化(例如,在进入main函数之前初始化)。为了记录类的唯一实例,很容易想到的就是使用静态成员变量,因此,在类的内部声明一个静态的Singleton指针,并将其构造函数声明为私有的即可。代码如下:

// .h
class Singleton
{
public:
	~Singleton();
	static Singleton* getInstance() { return m_uniqueInstance; }

private:
	Singleton() { std::cout << "Singleton created before running" << std::endl; }
	static Singleton* m_uniqueInstance;

	// 防止拷贝和赋值
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
};

// .cpp
Singleton* Singleton::m_uniqueInstance = new Singleton();

Singleton::~Singleton()
{
	std::cout << "Singleton deleted." << std::endl;
}

保存该类实例化的静态指针m_uniqueInstance在cpp中类的外部初始化,这样在程序运行之前该类的实例就已经生成好了。由于构造函数被声明为私有,因此该类无法在外部被构造,这样在整个程序运行期间该类就只拥有m_uniqueInstance指向的这一个实例。在C++11中还可使用delete禁止拷贝和赋值操作。

 

懒汉式(Lazily Initialized

即延迟初始化,与提前初始化方式不同,该方法仅在第一次调用 getInstance() 方法时才实例化这个对象,代码如下:

// .h
class LazySingleton
{
public:
	~LazySingleton();
	static LazySingleton* getInstance();

private:
	LazySingleton() { std::cout << "LazySingleton created before running" << std::endl; }
	static LazySingleton* m_uniqueInstance;

	// 防止拷贝和赋值
	LazySingleton(const LazySingleton&) = delete;
	LazySingleton& operator=(const LazySingleton&) = delete;

	static std::mutex m_mutex;
};

// .cpp
LazySingleton* LazySingleton::m_uniqueInstance = nullptr;
std::mutex LazySingleton::m_mutex;

LazySingleton::~LazySingleton()
{
	std::cout << "LazySingleton deleted." << std::endl;
}

LazySingleton* LazySingleton::getInstance()
{
	if (m_uniqueInstance == nullptr)        // 双重检查锁
	{
		m_mutex.lock();
		if (m_uniqueInstance == nullptr)
		{
			m_uniqueInstance = new LazySingleton();
		}
		m_mutex.unlock();
	}

	return m_uniqueInstance;
}

为了确保在多线程环境下正确调用 getInstance() 方法,这里使用了双重检查锁(DCLP)机制,即第一次判断指针时不加锁,因为只有在指针为空时才需要获取锁,由于此时可能有其它线程都通过了这一判断,因此判断完成后需要再检查一遍,并且让当前线程获取锁,以确保只有当前线程能进行实例化。

 

双重检查锁问题

这么做看似只有第一个调用 getInstance() 方法的线程能实例化一个单例,保证了线程安全,但双重检查锁机制本身也是存在问题的,主要问题就出在 m_uniqueInstance = new LazySingleton();

上述代码在双重检查后会new一个类的实例,而new操作做了以下三件事:

  1. LazySingleton 对象分配一片内存;

  2. 构造一个 LazySingleton 对象,存入已分配的内存区;

  3. m_uniqueInstance 指向这片内存区;

正常思路的new操作就是上述所描述的顺序那样,然而编译器有可能并不是按照上述步骤生成代码,他可能将步骤1和步骤3写成一条语句,然后才执行步骤2,这就导致上述代码变成:

LazySingleton* LazySingleton::getInstance() {
            if (m_uniqueInstance == nullptr) {
                m_mutex.lock();
                if (m_uniqueInstance == nullptr) {
                    m_uniqueInstance = // Step 3
                    operator new(sizeof(LazySingleton)); // Step 1
                    new (m_uniqueInstance) LazySingleton; // Step 2
                }
            }
            return m_uniqueInstance;
        }

这样的代码在多线程下就会有问题:

1. 线程A进入getInstance(),检查出m_uniqueInstance为空,请求加锁,而后执行由步骤1和步骤3组成的语句。之后线程A被挂起。此时,m_uniqueInstance已为非空指针,但m_uniqueInstance指向的内存里的LazySingleton对象还未被构造出来。

2. 线程B进入getInstance(), 检查出m_uniqueInstance非空,直接将m_uniqueInstance返回(return)给调用者,这样线程B就会得到一个未初始化过的LazySingleton对象,这显然是错误的。

 

解决方法

1.使用延迟初始化时,尽量在每个需要使用singleton对象的线程开始时,只调用一次instance(),之后该线程就可直接使用缓存在局部变量中的指针。

Singleton* const instance = Singleton::getInstance(); // cache instance pointer
 instance->func1(); 
 instance->func2();
 instance->func3(); 

而不是像这样

Singleton::getInstance()->func1(); 
Singleton::getInstance()->func2(); 
Singleton::getInstance()->func3(); 

2.使用提前初始化(eager initialization)方式,即在程序运行之初就对所需资源进行初始化(例如,在进入main函数之前初始化)。

3.使用 std::once_flag 和 std::call_once 进行延迟初始化,std::call_once 可以保证多个线程对函数只调用一次。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值