【设计模式】单例模式(Singleton)
目录
一、问题导入
二、概念
三、代码实现
1.设计思考
2.懒汉式vs饿汉式
四、常见问题
五、单例模式的优劣
1.优点
2.缺点
3.应用场景
前言:设计模式作为前人经验的总结,是一种可复用的解决某一类问题的方案。写博客的初衷是记录课上的学习内容,作为复习时的笔记。如果有错误,希望大家可以讨论/指正。此外,作为一个适用于所有语言的设计思想,我认为在学习过程中不应过度强调语言,但是因为课程老师是Java信徒,所以会不得不捎带一些语言方面的东西(基本可以不看,代码的话是C++的实现)。
Tips:不去实践,永远无法感受设计模式的魅力,希望大家不要像我一样学过就忘。
一、问题导入
单例模式(Singleton)可是说是新手在初步接触设计模式的时候最长使用的一类设计模式,他有效地解决了全局变量满天飞的情况,让我们的代码有了一点设计的雏形
考虑我们有一个全局变量,他将会在多个头文件中被使用到,但是如果只是单纯地定义一个全局变量,那在其他头文件使用的过程中,就需要使用外部关键字的引用extern,并且,每个头文件都需要加上这一行内容,这会导致代码冗余,降低可读性。
二、概念
单例模式是一种创建型设计模式,其核心是保证一个类在整个系统中只存在一个实例,并提供一个全局访问点来获取该实例。
三、代码实现
1.设计思考
那么问题就是如何保证该类在整个系统中只会存在一个实例。
由于只能存在一个实例,所以其不能通过外部调用构造函数或者拷贝构造函数去创建一个实例对象,因而就需要私有化构造函数,并对外暴露调用该实例对象的接口。
(1)私有化构造函数,析构函数,删除拷贝构造函数,赋值运算
(2)对外暴露调用实例的接口
2.懒汉式vs饿汉式
对于单例模式的实现,其有着两种最为简单的实现方式:①懒汉式②饿汉式
其中懒汉式只有在对该实例第一次进行调用的时候才会进行初始化,而饿汉式在程序的一开始时候就会完成初始化。
那么,很显然:
对于饿汉式,其优点就是在程序一开始便进行加载,不会受到多线程问题的影响。(在 C++11 及以后,全局 / 静态变量的初始化在多线程环境下是线程安全的(由编译器保证初始化过程的原子性))但是,也会因此降低程序的性能,导致了程序加载时间过长的问题。此外,如果该实例实际并没有使用到,也会导致资源的浪费。
对于懒汉式,其优点就是在使用的时候才会加载,减少了程序的加载时间,对性能更加友好,同时也避免了资源的浪费。但是,如果多个线程对当前实例同时进行访问的话,便会导致实例被多次初始化,产生多线程下的资源竞争问题。(仅在单线程下可以使用)
懒汉式的实现:
#pragma once
#include<iostream>namespace _SingletonPattern//这个只是我自己写的命名空间,不必理会,只看里面的内容就行了
{class Singleton{public://提供一个获取实例的方法static Singleton* GetInstance(){if (!instance)//懒汉式的实现方式,只有该实例没有创建的时候才进行创建instance = new Singleton;//实例已经创建过了的话就直接返回即可return instance;}//该实例的相关函数void on_run() { std::cout << "Singleton::on_run()" << std::endl; }/*省略*/ private://私有化构造函数与析构函数,禁用拷贝构造函数和赋值运算Singleton() = default;~Singleton() = default;Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;//唯一的一个实例对象static Singleton* instance;//还可以添加其他变量/*省略*/};Singleton* Singleton::instance = nullptr;
}
饿汉式的实现:
#pragma once
#include<iostream>namespace _SingletonPattern
{class Singleton{public:static Singleton* get_instance(){return instance;}void on_run() { std::cout << "Singleton::on_run()" << std::endl; }private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;static Singleton* instance;};Singleton* Singleton::instance = new Singleton();//直接进行初始化
}
调用方式:(_SingletonPattern是为了避免命名冲突所写的命名空间,可以不写)
#include<iostream>
#include"singleton.h"int main()
{//1.单例模式_SingletonPattern::Singleton::GetInstance()->on_run();return 0;
}
这样的话,在使用的时候只要包含对应的头文件,就能够通过类名直接调用该实例。
四、常见问题
单例模式面临着三个重要问题:
(1)多线程问题
(2)序列化/反序列化问题 (Java会考虑,C++非核心问题)
(3)反射问题 (Java会考虑,C++非核心问题)
这里我们主要考虑一下多线程的问题:
在上述的实现当中,我使用了懒汉式的加载方式,如果是多线程情况下,多个线程同时对该单例进行访问的话,就可能破坏掉原本只有一个实例的限制。在这里,我先列举单例模式的写法:
(1)饿汉式 (只可以解决多线程问题)
(2)懒汉式 (无法解决三个重要问题)
(3)双重检查锁 (只可以解决多线程问题)
(4)静态内部类 (只可以解决多线程问题)
(5)枚举类 (三个问题均可解决,Java可以考虑,C++实用性较低)
这里双重检查锁的实现代码段:(这里,通过加锁来保护临界区资源,并通过双重if判断优化了性能)
#include <atomic>
#include <mutex>class Singleton {
public:// 全局访问点:双重校验 + 原子操作static Singleton* GetInstance() {Singleton* inst = instance.load(std::memory_order_acquire);if (!inst) { // 第一次检查:无锁,快速返回std::lock_guard<std::mutex> lock(mtx); // 加锁inst = instance.load(std::memory_order_relaxed);if (!inst) { // 第二次检查:防止重复创建inst = new Singleton();instance.store(inst, std::memory_order_release);}}return inst;}// 禁用拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:Singleton() = default; // 私有构造~Singleton() = default;static std::atomic<Singleton*> instance; // 原子指针存实例static std::mutex mtx; // 互斥锁
};// 初始化静态成员
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;
五、单例模式的优劣
1.优点
(1)节省内存
(2)避免资源的重复使用
2.缺点
(1)无接口,无继承
(2)违背单一职责原则
3.应用场景
(1)应用程序需要一个且仅需要一个对象实例。此外,懒加载初始化和全局访问是必要的。
(2)当我们需要控制实例的数量并节省内存时。