设计模式——单例模式

单例模式是一种创建型设计模式,它可以确保一个类在整个程序运行过程中只有一个实例,并提供一个全局访问点以获取该实例。

单例模式的核心思想就是:控制对象的实例化,防止创建多个实例,从而节省资源并保证行为一致性。

关键点:

  • 单例类:包含单例实例的类,通常将构造函数声明为私有;
  • 静态成员变量:用于存储单例实例的静态成员变量;
  • 获取实例方法:静态方法,用于获取单例实例;
  • 私有构造函数:防止外部直接实例化单例类;
  • 线程安全处理:确保在多线程环境下单例实例的创建是安全的。
  • 构造函数和析构函数是私有的,不允许外部生成和释放
  • 静态成员变量和静态返回单例的成员函数
  • 禁用拷贝构造和赋值运算符

单例类主要是通过一个公共的静态方法getinstance接口,用于获取该类的实例,如果实例不存在,则在该方法内部创建实例并返回。

也就是说,单例类的构造方法不让其他人进行修改和使用;并且单例类只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,就只能进行调用,确保了全局只创建一个实例。

基本写法:

下面是一个最基础的实现:

cpp 复制代码
class Singleton
{
private:
    Singleton() {}        // 私有构造函数
    ~Singleton() {}       // 私有析构函数
    Singleton(const Singleton&) = delete;                // 禁用拷贝构造
    Singleton& operator = (const Singleton&) = delete;   // 禁用赋值操作符

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

    void show() {
        cout << "Singleton Show" << endl;
};

// 静态成员初始化
Singleton* Singleton::instance = nullptr;

int main() {
    Singleton::getInstance()->show();

    return 0;
}
    

其实他有两种方式懒汉模式和饿汉模式

懒汉模式

其核心是延迟初始化,可以理解为它很懒,所以它一直没有初始化,只有在首次调用getInstance()时才创建单例实例。

  • 优点:节省资源,单例对象未被使用,则不会创建。
  • 缺点:需要考虑线程安全的问题,多线程下可能会重复创建。

饿汉模式

其核心是提前初始化,即在程序启动时直接创建单例实例,无论是否被使用。

  • 优点:线程安全,实例在程序启动时初始化,避免多线程竞争。
  • 缺点:浪费资源,即使没有使用单例对象,也会占用内存。

实际开发中建议使用C++11的局部静态变量懒汉模式

实现过程分析

下面是针对懒汉模式的一个实现过程的分析

示例1

cpp 复制代码
class Singleton1 {
public:
    // 要点2
	static Singleton1 * GetInstance() {
		if(_instance == nullptr) {
			_instance = new Singleton1();
		}
		return _instance;
	}
private:
    // 要点1
	Singleton1() {}
	~Singleton1() {
		std::cout << "~Singleton1()\n";
	}
    // 要点3
	Singleton1(const Singleton1 &) = delete;
	Singleton1& operator = (const Singleton1&) = delete;
	Singleton1(Singleton1 &&) = delete;
	Singleton1& operator = (Singleton1 &&) = delete;
	// 要点2
	static Singleton1 *_instance; 
};
Singleton1* Singleton1::_instance = nullptr;

错误的点:_instance = new Singleton1();堆上的资源不能得到正确的析构,资源还是可以释放,但是在释放的时候是无法调用析构函数的。

示例2

cpp 复制代码
class Singleton2 {
public:
	static Singleton2 * GetInstance() {
		if(_instance == nullpte) {
			_instance = new Singleton2();
			atexit(Destructor);
		}
		return _instance;
	}
private:
	static void Destructor() {
		if(nullptr != _instance) {
			delete _instance;
			_instance = nullptr;
		}
	}
	Singleton2() {}
	~Singleton2() {
		std::cout << "~Singleton2()\n";
	}
	Singleton2(const Singleton2 &) = delete;
	Singleton2& operator = (const Singleton2&) = delete;
	Singleton2(Singleton2 &&) = delete;
	Singleton2& operator = (Singleton2 &&) = delete;
	
    static Singleton2 *_instance; 
};
Singleton2* Singleton2::_instance = nullptr;

相对于示例1,添加了一个atexit()方法,这个方法就是程序退出的时候,它会去调用Destructor函数,就可以在这个函数里面实现一个手动的析构

示例3

cpp 复制代码
class Singleton3 {
public:
	static Singleton3 * GetInstance() {
		std::lock_guard<std::mutex> lock(_mutex);
		if(_instance == nullptr) {
			std::lock_guard<std::mutex> lock(_mutex);
			if(_instance == nullptr) {
				_instance = new Singleton3();
				// 1. 分配内存
				// 2. 调用构造函数
				// 3. 返回对象指针 
				atexit(Destructor);
			}
		}
		return _instance;
	}
private:
	static void Destructor() {
		if(nullptr != _instance) {
			delete _instance;
			_instance = nullptr;
		}
	}
	Singleton3() {}
	~Singleton3() {
		std::cout << "~Singleton3()\n";
	}
	Singleton3(const Singleton3 &) = delete;
	Singleton3& operator = (const Singleton3&) = delete;
	Singleton3(Singleton3 &&) = delete;
	Singleton3& operator = (Singleton3 &&) = delete;
	
	static Singleton3 *_instance; 
	static std::mutex _mutex;
};
Singleton3* Singleton3::_instance = nullptr;
std::mutex Singleton3::_mutex;

要实现一个线程安全的单例模式,如果只用if外面那个锁,它是单检测的情况下,它总是返回那个_instance是线程安全的,缺点是,new Singleton3()只会有一次,其他情况都是拿到instance然后进行返回。两个if中间加锁是为了防止有两个线程同时进入而导致new了两次。但是也不对,没有考虑到多线程情况下,指令重排的问题。

示例4

cpp 复制代码
class Singleton4 {
public:
	static Singleton4 * GetInstance() {
		Singleton4* tmp = _instance.load(std::memory_order_relaxed);
		std::atomic_thread_fence(std::memory_order_acquire);
		if(tmp == nullptr) {
			std::lock_guard<std::mutex> lock(_mutex);
			tmp = _instance.load(std::memory_order_relaxed);
			if(tmp == nullptr) {
				tmp = new Singleton4();
				std::atomic_thread_fence(std::memory_order_release);
				_instance.store(tmp, std::memory_order_relaxed);
				atexit(Destructor);
			}
		}
		return tmp;
	}
private:
	static void Destructor() {
		Singleton4* tmp = _instance.load(std::memory_order_relaxed);
		if(nullptr != tmp) {
			delete tmp;
		}
	}
	Singleton4() {}
	~Singleton4() {
		std::cout << "~Singleton4()\n";
	}
	Singleton4(const Singleton4 &) = delete;
	Singleton4& operator = (const Singleton4&) = delete;
	Singleton4(Singleton4 &&) = delete;
	Singleton4& operator = (Singleton4 &&) = delete;
	
	static std::atomic<Singleton4*> _instance;
	static std::mutex _mutex;
};
std::atomic<Singleton4*> Singleton4::_instance;
std::mutex Singleton4::_mutex;

强制限制指令重排。

里面加入了获取屏障acquire fence和释放屏障release fence,也就后续的读/写操作不会重排到屏障前,且能读取其他线程的释放操作结果;释放屏障前面的读/写操作不会重排到屏障后,且保证当前线程的写入对其他线程可见。

示例5

cpp 复制代码
class Singleton5 {
public:
	static Singleton5* GetInstance() {
		static Singleton5 instance;
		return &instance;
	}
private:
	Singleton5() {}
	~Singleton5() {
		std::cout << "~Singleton5()\n";
	}
	
	Singleton5(const Singleton5 &) = delete;
	Singleton5& operator = (const Singleton5&) = delete;
	Singleton5(Singleton5 &&) = delete;
	Singleton5& operator = (Singleton5 &&) = delete;
};

这个版本是最简单的,如果只是让你简单实现一个单例模式,可以直接写这个版本。

示例6

cpp 复制代码
template<typename T>
class Singleton {
public:
	static T* GetInstance() {
		static T instance;
		return &instance;
	}
protected:
	Singleton() {}
	virtual ~Singleton() {
		std::cout << "~Singleton()\n";
	}
private:
	Singleton(const Singleton &) = delete;
	Singleton& operator = (const Singleton&) = delete;
	Singleton(Singleton &&) = delete;
	Singleton& operator = (Singleton &&) = delete;
};

class DesignPattern : public Singleton<DesignPattern> {
	friend class Singleton<DesignPattern>;
private:
	DesignPattern() {}
	~DesignPattern() {
		std::cout << "~DesignPattern()\n";
	}
};

使用类模板把三个要点进行封装

总结:

版本1:堆上资源不能正确析构(没有调用析构函数)

版本2:堆上资源能正确析构(调用了析构函数)

版本3:双检查锁,可能造成内存泄露

版本4:线程安全,原子操作+互斥锁+内存屏障

版本5:C++11静态局部变量具备线程安全特性延迟加载内存正确释放

版本6:模板类封装了单例要点

相关推荐
大飞pkz2 小时前
【设计模式】享元模式
开发语言·设计模式·c#·享元模式
Java&Develop2 小时前
GitLab-如何基于现有项目仓库,复制出新的项目仓库
java
一只乔哇噻2 小时前
java后端工程师进修ing(研一版‖day49)
java·开发语言
稻草猫.3 小时前
Java线程安全:volatile与wait/notify详解
java·后端·idea
无敌最俊朗@3 小时前
MQTT 关键特性详解
java·前端·物联网
JAVA学习通3 小时前
微服务项目->在线oj系统(Java-Spring)----[前端]
java·开发语言·前端
拾贰_C3 小时前
【SpringBoot】前后端联动实现条件查询操作
java·spring boot·后端
GUIQU.5 小时前
【QT】嵌入式开发:从零开始,让硬件“活”起来的魔法之旅
java·数据库·c++·qt
callJJ9 小时前
从 0 开始理解 Spring 的核心思想 —— IoC 和 DI(2)
java·开发语言·后端·spring·ioc·di