单例模式(Singleton):全局唯一实例的受控入口
更新时间:2026-08-25。本文按"先看不用模式的困境 → 再用模式重构 → 类图对比分析"的结构,论述单例模式。可运行代码:demos/design-patterns/singleton。
一、问题场景:系统里"只该有一份"的资源
日志器、配置中心、连接池、全局计数器——这类资源在系统中逻辑上只允许存在一份,且要被多个模块共享访问。核心矛盾是:共享性要求"全局可达",唯一性要求"不可随意复制"。
二、不使用模式:两种朴素方案及其缺陷
方案 A:全局静态对象
cpp
// config.h —— 全局单份配置
struct Config { int timeout; };
extern Config g_config; // 全局定义| 缺陷 | 说明 |
|---|---|
| 初始化顺序不可控 | 多个全局对象互相依赖时,构造顺序未定义(static initialization order fiasco) |
| 无延迟加载 | 进程一启动就构造,即使没人用也占用资源 |
| 无构造控制 | 任何人都能再 new 一份,全局的"唯一性"靠自觉 |
| 难以测试 | 全局状态污染测试用例之间 |
方案 B:参数传递
cpp
void worker(Config& cfg) { ... } // 每个函数都要带 cfg 参数
int main() { Config cfg; worker(cfg); }| 缺陷 | 说明 |
|---|---|
| 参数泛滥 | 与业务无关的 cfg 穿遍所有函数签名,改动波及面大 |
| 创建权分散 | 谁负责创建、谁负责释放,职责不清 |
不使用模式的类图

三、使用模式:受控的唯一实例
单例模式把"唯一性"变成类自身保证:构造私有化 + 静态 getInstance() 返回唯一实例。C++ 的经典线程安全写法(C++11 起,局部静态变量初始化天然线程安全):
cpp
// config.h
class Config {
public:
static Config& instance(); // 唯一入口
int timeout() const { return timeout_; }
void set_timeout(int v) { timeout_ = v; }
private:
Config() : timeout_(3000) {} // 构造私有
Config(const Config&) = delete; // 禁拷贝
Config& operator=(const Config&) = delete;
int timeout_;
};
// config.cpp
Config& Config::instance() {
static Config inst; // 延迟构造 + 线程安全
return inst;
}
// 使用:Config::instance().set_timeout(5000);使用模式的类图

四、类图对比与分析
| 维度 | 不使用(全局对象/参数) | 使用单例 |
|---|---|---|
| 唯一性保证 | 靠约定 | 构造私有化强制 |
| 初始化时机 | 静态期(顺序不可控) | 首次 instance() 调用(延迟) |
| 线程安全 | 全局初始化竞态 | 局部静态变量天然安全 |
| 访问成本 | 全局符号/参数传递 | 一次静态方法调用 |
| 可测试性 | 全局状态污染 | 提供 reset()/可替换实例(测试版) |
| 代码改动 | 参数签名全链路传播 | 调用方零改动 |
分析结论:单例的收益集中在"全局共享 + 唯一性受控"。但它是最常被误用的模式——若实例并非"逻辑唯一",或"每线程一份"更合适(如线程局部日志),单例就是错的抽象。
五、适用边界与常见误用
| 适用 | 不适用 |
|---|---|
| 全局唯一且共享:配置、日志器、连接池 | 需要多实例的普通组件(应 DI 注入) |
| 跨模块共享的"读多写少"状态 | 每线程独立的上下文(应 thread_local) |
| 进程级单份资源 | 可测试性要求极高、依赖注入架构(避免全局) |
反模式警示:单例 + 可变全局状态 = 隐藏的全局变量,会让测试互相污染、并发 bug 难以定位。现代工程倾向"依赖注入 + 容器管理单例生命周期"(IoC 容器里的 singleton scope),既保留唯一性又保住可测试性。
一句话总结
单例模式用"构造私有 + 静态入口"把全局唯一性从约定变成强制,换来延迟初始化与线程安全;适用前提是"逻辑上确实唯一且共享",否则应退回依赖注入,避免制造可变的全局状态。