设计模式——单例模式Singleton

参考:
C++ 单例模式总结与剖析——这篇文章写得很好。

单例模式
——这篇文章的代码很详细。

1.单例模式简介

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
在这里插入图片描述
要点:
1)全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
2)线程安全
3)禁止赋值和拷贝
4)用户通过接口获取实例:使用 static 类成员函数

2.实现方式

2.1 饿汉式

饿汉式就是静态成员直接初始化,这样就保证了静态变量只被初始化一次,可以确保多个线程使用的是同一个单例对象。
缺点是在不使用单例的时候也需要进行初始化,内存占用较大。
示例代码:

#include <iostream>

class Singleton
{
private:
	Singleton() {
		std::cout << "constructor called\n";
	}
	~Singleton() {
		std::cout << "destructor called\n";
	}
	Singleton(Singleton&) = delete;//拷贝构造函数
	Singleton& operator=(const Singleton&) = delete;//赋值运算符重载

	static Singleton* m_pSingleton;
public:
	static Singleton* getInstance() {
		return m_pSingleton;
	}
};

Singleton* Singleton::m_pSingleton = new Singleton;

int main(int argc, char const *argv[])
{
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
	return 0;
}

执行结果:
在这里插入图片描述

2.2 懒汉式

2.2.1 有缺陷的懒汉模式

懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象。好处是如果被调用就不会占用内存。

#include <iostream>

class Singleton
{
private:
	Singleton() {
		std::cout << "constructor called\n";
	}
	~Singleton() {
		std::cout << "destructor called\n";
	}
	Singleton(Singleton&) = delete;//拷贝构造函数
	Singleton& operator=(const Singleton&) = delete;//赋值运算符重载

	static Singleton* m_Instance;
public:
	static Singleton* getInstance() {
		if(m_Instance == nullptr)//这里会导致线程不安全
			m_Instance = new Singleton;
		return m_Instance;
	}

	static void Delete() {
		if(m_Instance)
			delete m_Instance;
		m_Instance = nullptr;
	}
};

Singleton* Singleton::m_Instance = nullptr;

int main(int argc, char const *argv[])
{
	Singleton* p1 = Singleton::getInstance();
	Singleton* p2 = Singleton::getInstance();
	std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
	return 0;
}

在单线程时,该程序可以正常运行,但是在多线程时,会出现错误,因为多个线程可能同时判断m_Instancenullptr,然后执行m_Instance = new Singleton;

#include <iostream>
#include <thread>

class Singleton
{
private:
	Singleton() {
		std::cout << "constructor called\n";
	}
	~Singleton() {
		std::cout << "destructor called\n";
	}
	Singleton(Singleton&) = delete;//拷贝构造函数
	Singleton& operator=(const Singleton&) = delete;//赋值运算符重载

	static Singleton* m_Instance;
public:
	static Singleton* getInstance() {
		if(m_Instance == nullptr)
			m_Instance = new Singleton;
		return m_Instance;
	}

	static void Delete() {
		if(m_Instance)
			delete m_Instance;
		m_Instance = nullptr;
	}
};

Singleton* Singleton::m_Instance = nullptr;

static Singleton *p1, *p2; 
void fun1()
{
	p1 = Singleton::getInstance();
}

void fun2()
{
	p2 = Singleton::getInstance();
}

int main(int argc, char const *argv[])
{
	int times = 0;
	while(1){
		std::thread* th1 = new std::thread(fun1);
		std::thread* th2 = new std::thread(fun2);
		std::this_thread::sleep_for(std::chrono::milliseconds(100));

		if(p1 && p2){
			if(p1 != p2){
				break;//两者不同,说明Singleton::getInstance()中的new Singleton执行了两次
			}
			else{
				Singleton::Delete();//删除原来的
				times++;
				delete th1;
				delete th2;
				continue;//继续执行循环,直到在多线程中出现new Singleton执行了两次的情况
			}
		}	
	}
	std::cout << "try times = " << times << "\n";
	std::cout << "p1 = " << p1 << "\t" << "p2 = " << p2 << "\n";
	return 0;
}

在这里插入图片描述
可以看出上述代码在不满足线程安全的要求,在多线程的情况下可能会出现错误。
在这里插入图片描述

2.2.2 双重锁加智能指针

双重锁解决并发问题,智能指针解决内存泄漏问题。

#include <iostream>
#include <memory> //shared_ptr
#include <mutex> //mutex

class Singleton
{
private:
	Singleton(){ std::cout << "constructor called\n"; }
	~Singleton(){ std::cout << "destructor called\n"; }
	Singleton(Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

private:
	static std::shared_ptr<Singleton*> m_instance_ptr;
	static std::mutex m_mutex;

public:
	static std::shared_ptr<Singleton*> get_instance(){
		//双重锁
		if(m_instance_ptr == nullptr){
			std::lock_guard<std::mutex> lk(m_mutex);
			if(m_instance_ptr == nullptr){
				m_instance_ptr = std::shared_ptr<Singleton*>(new Singleton);
			}
		}
		return m_instance_ptr;
	}
};

std::shared_ptr<Singleton*> Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::mutex;

int main(){
	std::shared_ptr<Singleton*> p1 = Singleton::get_instance();
	std::shared_ptr<Singleton*> p2 = Singleton::get_instance();
	return 0;
}

shared_ptr和mutex都是C++11的标准,以上这种方法的优点是
基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。

还有更加严重的问题,在某些平台(与编译器和指令集架构有关),双检锁会失效!这是由于编译器编译完的执行顺序与我们理解的执行顺序并不一致。具体参考C++多线程——原子操作atomic对内存模型的介绍。

2.2.3 推荐的懒汉式单例——使用局部静态变量
#include <iostream>

class Singleton
{
private:
	Singleton() {
		std::cout << "constructor called\n";
	}
	~Singleton() {
		std::cout << "destructor called\n";
	}
	Singleton(Singleton&) = delete;//拷贝构造函数
	Singleton& operator=(const Singleton&) = delete;//赋值运算符重载

public:
	static Singleton& getInstance() {
		static Singleton m_Instance;
		return m_Instance;
	}
};

int main(int argc, char const *argv[])
{
	Singleton& instance_1 = Singleton::getInstance();
    Singleton& instance_2 = Singleton::getInstance();
	std::cout << "instance_1's address" << &instance_1 << "\t" << "instance_2's address " << &instance_2 << "\n";
	return 0;
}

在这里插入图片描述

局部静态变量不仅只会初始化一次,而且还是线程安全的。
注意getInstance函数现在返回的是引用。

这种单例被称为Meyers’ Singleton。这种方法很简洁,也很完美,但是注意:
gcc 4.0之后的编译器支持这种写法。
C++11及以后的版本(如C++14)的多线程下,正确。
C++11之前不能这么写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值