设计模式(C++)-创建型模式-单例模式

一、单例模式概述

  • 单例模式(Singleton Pattern)是一种创建型设计模式,其核心目的是确保一个类在整个应用程序生命周期内最多只有一个实例,并为该实例提供一个全局唯一的访问入口。这种模式主要用于管理那些需要集中控制、资源共享或状态一致的系统组件,如配置管理器、线程池或日志服务。
  • 实现单例模式的步骤:
    1、构造函数私有化
    2、增加静态私有的当前类的指针变量
    3、提供静态对外接口,可以让用户获得单例对象
    单例分为懒汉式和饿汉式
    懒汉式:解决了饿汉式内存浪费问题,但是线程不安全的,可以通过互斥量mutex.lock()和mutex.unlock()来解决
    饿汉式:还没有使用该单例对象,该单例对象就已经被加载到内存了,在对象过多时会造成内存浪费

二、单例模式UML类图

单例模式

三、单例模式代码实现

3.1 懒汉式

//singletonlazy.h
class singletonlazy {
public:
	static singletonlazy* getInstance();
	static void releaseInstance();
private:
	singletonlazy() {};
	static singletonlazy* psingleton;
	static std::mutex mtx;
};

//singletonlazy.cpp
//这段代码在多线程下不安全,由于指令重排,new可能先返回指针(写入 instance)再执行构造,
//其他线程在①看到非空但对象未完成构造
#include <iostream>
using namespace std;
singletonlazy* singletonlazy::psingleton = nullptr;
singletonlazy* singletonlazy::getInstance() {
	if (nullptr == psingleton) { //① 第一次检查
		std::lock_guard<std::mutex> lock(mtx);
		if (nullptr == psingleton) {// ② 第二次检查
			psingleton = new singletonlazy();
		}
	}
	return psingleton;
}
void singletonlazy::releaseInstance() {
	if (nullptr != psingleton) {
		delete psingleton;
		psingleton = nullptr;
	}
}

C++11之后,用 std::atomic<Singleton*>+ 合适的内存序(Release/Acquire),或直接用 局部静态变量(Meyers’ Singleton),编译器/运行时自动处理线程安全。
C++11 标准实现(DCLP)

//C++11 标准实现(DCLP)
class singletonlazyC11 {
public:
	static singletonlazyC11* getInstance();
	static void releaseInstance();
private:
	static std::atomic<singletonlazyC11*> psingleton;
	static std::mutex mtx;

	singletonlazyC11() = default;
	~singletonlazyC11() = default;
	// 禁止拷贝
	singletonlazyC11(const singletonlazyC11&) = delete;
	singletonlazyC11& operator=(const singletonlazyC11&) = delete;
};
//singletonlazyC11.cpp
//C++11 标准实现(DCLP)
// 静态成员初始化
std::atomic<singletonlazyC11*> singletonlazyC11::psingleton{ nullptr };
std::mutex singletonlazyC11::mtx;
singletonlazyC11* singletonlazyC11::getInstance() {
	// 第一步:无锁快速检查(放松内存序)
	singletonlazyC11* tmp = psingleton.load(std::memory_order_relaxed);
	// 编译器屏障(防止重排,x86可省略,ARM等弱内存模型需保留)
	std::atomic_signal_fence(std::memory_order_acquire);
	if (nullptr == psingleton) {
		// 进入临界区
		std::lock_guard<std::mutex> lock(mtx);
		// 第二步:持锁后再次检查(Acquire语义读取)
		tmp = psingleton.load(std::memory_order_acquire);
		if (nullptr == psingleton) {
			// 创建对象(此时必然完成构造)
			tmp = new singletonlazyC11();
			// 第三步:释放语义写入指针(确保构造完成才可见)
			psingleton.store(tmp, std::memory_order_release);
		}
	}
	return tmp;
}
void singletonlazyC11::releaseInstance() {
	if (nullptr != psingleton) {
		delete psingleton;
		psingleton = nullptr;
	}
}

3.2 饿汉式

//饿汉式 对象在程序执行时优先创建
//饿汉式是线程安全的
class singletonhungry {
public:
	static singletonhungry* getInstance();
	static void releaseInstance();
private:
	singletonhungry() {};
	static singletonhungry* psingleton;
};
singletonhungry* singletonhungry::psingleton = new singletonhungry();
singletonhungry* singletonhungry::getInstance() {
	return psingleton;
}
void singletonhungry::releaseInstance() {
	if (nullptr != psingleton) {
		delete psingleton;
		psingleton = nullptr;
	}
}

3.3 C++11之后更推荐静态变量写法

static Singleton& getInstance() {
    static Singleton instance;
    return instance;
}

四、优缺点总结

- 核心优点:不可替代的刚性价值

1.严格物理唯一性 ,保障状态权威
对于必须独占或共享的物理资源/全局控制器,单例提供了比松散全局变量更强的约束。它能确保像数据库连接池、硬件设备句柄、全局配置映射这类对象在全系统只存在一份真实来源,彻底消除多副本导致的数据不一致风险。
2.惰性加载与资源优化
配合“懒汉式”实现,只有在首次被请求时才初始化。这对高开销对象(如加载大型词典、建立网络长连接)至关重要,避免了程序启动时的冷启动瓶颈,真正做到“用时方建”。
3.访问便利性与依赖收缩
相比于将通用服务(如日志器)通过参数层层传递(污染函数签名),单例提供了扁平化的访问入口。这在横切关注点(Cross-cutting Concerns)中减少了中间层的冗余透传,降低了短期开发成本。

结构性缺陷:

1.严重破坏可测试性(单例携带隐式全局状态。单元测试中,A测试修改单例状态会导致B测试出现偶发失败(测试隔离失效)。Mock替换极其困难,迫使测试代码依赖真实的磁盘/网络IO,使其从“单元测试”退化为“集成测试”,破坏了自动化测试的根基。)
2.违反单一职责原则(经典单例类承担了双重职责:自身业务逻辑​ + 实例生命周期管理。这种“自举式设计”导致类边界模糊,随着功能膨胀,代码变得难以拆分和维护)
3.隐式耦合与依赖黑洞(方法签名中看不到单例依赖(void func()vs void func(ILogger&)),依赖关系被隐藏。这导致代码逻辑晦涩难懂,重构时极易遗漏依赖链条,引发运行时错误。)
4.并发风险与生命周期困境 (即使通过DCL或原子变量解决构造竞态,析构时的竞态条件依然棘手。若线程A正在访问单例,线程B触发了析构(如退出作用域),将导致野指针或资源双重释放。多线程环境下的安全销毁往往需要引入引用计数或守护机制,复杂度极高。)

更多推荐