C++之单例模式

C++之单例模式

  • 前言
  • 介绍
    • 1、单例模式是什么?
      • [1.1 实现单例模式的三个要点](#1.1 实现单例模式的三个要点)
      • [1.2 单例模式分类](#1.2 单例模式分类)
    • [2. 懒汉式](#2. 懒汉式)
      • [2.1 懒汉实现:基础方法](#2.1 懒汉实现:基础方法)
      • [2.2 懒汉实现:基于单锁](#2.2 懒汉实现:基于单锁)
      • [2.3 懒汉实现:基于双重检测锁](#2.3 懒汉实现:基于双重检测锁)
      • [2.4 懒汉实现:基于双重检测锁和资源管理](#2.4 懒汉实现:基于双重检测锁和资源管理)
        • [2.4.1 智能指针方式](#2.4.1 智能指针方式)
        • [2.4.2 静态嵌套类方式](#2.4.2 静态嵌套类方式)
      • [2.5 懒汉实现:基于局部静态对象](#2.5 懒汉实现:基于局部静态对象)
    • [3. 饿汉式](#3. 饿汉式)
      • [3.1 饿汉实现:基础方法](#3.1 饿汉实现:基础方法)
    • [4. 总结](#4. 总结)

前言

单例模式(Singleton Pattern)是 面向对象中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。

优点:通过单例模式的设计,使得创建的类在当前进程中只有一个实例,并提供一个全局性的访问点,这样可以规避因频繁创建对象而导致的 内存飙升 情况。

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

1、单例模式是什么?

在面向对象编程中,有时候我们希望一个类只有一个实例化的对象,比如线程池,缓存等。这些类有且只有一个唯一的实例,这种设计模式被称为单例模式。

1.1 实现单例模式的三个要点

1)私有化构造函数:这样外界就无法自由地创建类对象,进而阻止了多个实例的产生。

2)类定义中含有该类的唯一静态私有对象:静态变量存放在全局存储区,且是唯一的,供所有对象使用。

3)用公有的静态函数来获取该实例:提供了访问接口。

1.2 单例模式分类

单例模式有两种主要实现方法:懒汉模式和饿汉模式。

  1. 懒汉模式特点是当外界调用时才进行实例化;
  2. 饿汉模式特点是一开始就对实例进行初始化,调用时直接返回这个构建好的实例。

2. 懒汉式

懒汉模式特点是当外界调用时才进行实例化。

2.1 懒汉实现:基础方法

是否多线程安全:否

实现难度:易

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

缺点:一个是线程安全,另一个是内存泄漏。

线程安全是因为在多线程场景下,有可能出现多个线程同时进行new操作的情况,没通过加锁来限制。

内存泄漏是因为使用了new在堆上分配了资源,那么在程序结束时,也应该进行delete,确保堆中数据释放。

cpp 复制代码
public class Singleton {  
    // 静态私有对象
    private static Singleton instance;  
    // 私有构造函数
    private Singleton (){}  
  
  // 公有接口获取唯一实例
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

这种方式在单线程下没有问题,但是如果多线程模式下,当唯一实例还没有创建,两个线程同时调用getinstance就可能同时创建对象,导致错误。

2.2 懒汉实现:基于单锁

是否多线程安全:是

实现难度:较易

描述:这种方式采用单锁机制,有可能造成阻塞。

cpp 复制代码
Singleton*Singleton::getInstance(){
    m.lock();
    if (_instance == nullptr)
        _instance = new Singleton;
    m.unlock();
    return _instance;
}

加锁又会带来另外的性能问题,如果每个线程每次获取实例都加锁,有可能造成阻塞的发生。实际上,上锁的目的是为了防止有多个线程在实例未被初始化的情况下,同时对他进行初始化,如果实例已经被创建了,就不需要考虑这个问题了,所以就可以采用二次加锁的方法来提高程序的性能。

2.3 懒汉实现:基于双重检测锁

是否多线程安全:是

实现难度:较复杂

描述:这种方式采用双锁机制,可以确保线程安全,且在多线程情况下能保持高性能。

getInstance() 的性能对应用程序很关键。

cpp 复制代码
Singleton*Singleton::getInstance(){

    if (_instance == nullptr)
    {
        m.lock();
        if (_instance == nullptr)
        {
            _instance = new Singleton;
        }
        m.unlock();
    }
    return _instance;
}

接下来,我们再解决内存泄漏(资源释放)问题,对懒汉式实现进行进一步的改进。

2.4 懒汉实现:基于双重检测锁和资源管理

是否多线程安全:是

实现难度:较复杂

描述:这种方式采用双锁机制,可以确保线程安全,且在多线程情况下能保持高性能。并且加入资源管理机制,以达到对资源的释放的目的。

我们加入资源管理机制,以达到对资源的释放的目的,解决方法有两个:智能指针&静态嵌套类。

2.4.1 智能指针方式

将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
	static shared_ptr<Singleton> getInstance() {
// 若为空则创建
		if (instance == nullptr) {
// 加锁保证线程安全
// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
		lock_guard<mutex> l(m_mutex);
		if (instance == nullptr) {
			cout << "实例为空,开始创建。" << endl;
			instance.reset(new Singleton(), destoryInstance);
			cout << "地址为:" << instance << endl;
			cout << "创建结束。" << endl;
		}
	}
		else {
			cout << "已有实例,返回。" << endl;
		}
	return instance;
}
// 毁灭实例
static void destoryInstance(Singleton* x) {
	cout << "自定义释放实例" << endl;
	delete x;
}

private:
// 私有构造函数
Singleton() {
	cout << "构造函数启动。" << endl;
};

// 私有析构函数
~Singleton() {
	cout << "析构函数启动。" << endl;
};

private:
// 静态私有对象
static shared_ptr<Singleton> instance;
// 锁
static mutex m_mutex;
};

// 初始化
shared_ptr<Singleton> Singleton::instance;
mutex Singleton::m_mutex;

应用智能指针后,在程序结束时,它自动进行资源的释放,解决了内存泄漏的问题。

2.4.2 静态嵌套类方式

类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton* getInstance() {
// 若为空则创建
	if (instance == nullptr) {
// 加锁保证线程安全
// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
		lock_guard<mutex> l(m_mutex);
		if (instance == nullptr) {
			cout << "实例为空,开始创建。" << endl;
			instance = new Singleton();
			cout << "地址为:" << instance << endl;
			cout << "创建结束。" << endl;
			}
	}
		else {
			cout << "已有实例,返回。" << endl;
			}
	return instance;
}

private:
// 私有构造函数
Singleton() {
	cout << "构造函数启动。" << endl;
	};

// 私有析构函数
~Singleton() {
cout << "析构函数启动。" << endl;
};

// 定义一个删除器
class Deleter {
public:
Deleter() {};
~Deleter() {
if (instance != nullptr) {
	cout << "删除器启动。" << endl;
	delete instance;
	instance = nullptr;
	}
}
};

// 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
static Deleter m_deleter;
private:
// 静态私有对象
static Singleton* instance;
// 锁
static mutex m_mutex;
};

// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
Singleton::Deleter Singleton::m_deleter;

2.5 懒汉实现:基于局部静态对象

是否多线程安全:是

实现难度:一般

描述:C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的。

因为有上述的改动,所以出现了一种更简洁方便优雅的实现方法,基于局部静态对象实现。

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton& getInstance() {
	cout << "获取实例" << endl;
	static Singleton instance;
	cout << "地址为:" << &instance << endl;
	return instance;
	}
private:
// 私有构造函数
Singleton() {
	cout << "构造函数启动。" << endl;
	};

// 私有析构函数
~Singleton() {
	cout << "析构函数启动。" << endl;
	};
};

3. 饿汉式

饿汉模式特点是一开始就对实例进行初始化,调用时直接返回这个构建好的实例。

3.1 饿汉实现:基础方法

是否多线程安全:是

实现难度:易

描述:这种方式比较常用,但容易产生垃圾对象。

优点:没有加锁,执行效率会提高。第一次调用才初始化,避免内存浪费。

缺点:类加载时就初始化,浪费内存。必须加锁 synchronized 才能保证单例,但加锁会影响效率。

它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

cpp 复制代码
#include <iostream>
#include <mutex>
using namespace std;

// 单例模式演示类
class Singleton
{
public:
// 公有接口获取唯一实例
static Singleton* getInstance() {
	cout << "获取实例" << endl;
	cout << "地址为:" << instance << endl;
	return instance;
	}
private:
// 私有构造函数
Singleton() {
	cout << "构造函数启动。" << endl;
};

// 私有析构函数
~Singleton() {
	cout << "析构函数启动。" << endl;
};

private:
// 静态私有对象
static Singleton* instance;
};

// 初始化
Singleton* Singleton::instance = new Singleton();

main还没开始,实例就已经构建完毕,获取实例的函数也不需要进行判空操作,因此也就不用双重检测锁来保证线程安全了,它本身已经是线程安全状态了。

但是内存泄漏的问题还是要解决的,这点同懒汉是一样的。可以通过智能指针和静态嵌套实现。

4. 总结

一般情况下,建议使用基于双重检测锁和资源管理搭配智能指针的懒汉方式。

相关推荐
橘色的喵几秒前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
何曾参静谧36 分钟前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
AI街潜水的八角1 小时前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
JSU_曾是此间年少1 小时前
数据结构——线性表与链表
数据结构·c++·算法
此生只爱蛋2 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
何曾参静谧2 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
lulu_gh_yu3 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法
ULTRA??4 小时前
C加加中的结构化绑定(解包,折叠展开)
开发语言·c++
凌云行者4 小时前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者5 小时前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl