C++ 单例模式硬核通关:从“只创建一个对象”到真正写对企业级 Singleton

很多初学者第一次接触单例模式,脑子里通常只有一句话:

单例模式就是保证一个类只有一个对象。

这句话没错,但太浅了。

真正写代码时,你很快会遇到一堆更具体的问题:

  • 为什么构造函数要写成 private
  • 只把构造函数私有化就够了吗?
  • 为什么还要禁用拷贝构造和赋值运算符?
  • static Singleton instance; 这个对象到底放在栈上、堆上,还是全局区?
  • ~Singleton() = default; 是不是“不写析构”,会不会不释放资源?
  • 多线程同时调用 GetInstance() 会不会创建多个对象?
  • 配置文件、日志系统这种全局资源,到底适不适合用单例?

这篇文章就从这些真实问题出发,把 C++ 单例模式从基础写法一路讲到推荐实现、配置文件实战、常见误区和复习速查表。

目标很明确:你看完之后,不只是会背 Singleton,而是能在真实项目里判断该不该用、怎么写、哪里会炸。


一、引言:为什么你以为会写单例,其实很容易写错

假设项目里有一个配置文件 camera.toml,里面保存相机分辨率、帧率、曝光参数等信息。

很多模块都要读取它:

auto width  = config["camera"]["width"];
auto height = config["camera"]["height"];
auto fps    = config["camera"]["fps"];

这时候你肯定不希望每个模块都自己解析一遍配置文件。

因为这样会带来几个问题:

问题 后果
重复加载配置文件 浪费 IO 和解析成本
多处维护配置对象 状态容易不一致
每个模块都能随便创建配置对象 代码边界混乱
多线程同时初始化配置 可能出现竞态条件

所以我们自然会想到:

配置对象全程序只需要一个,并且所有地方都通过同一个入口访问它。

这就是单例模式最典型的使用场景。

但问题来了,很多人第一版会这么写:

class Config
{
public:
    static Config* GetInstance()
    {
        if (instance_ == nullptr)
        {
            instance_ = new Config();
        }
        return instance_;
    }

private:
    Config() = default;

private:
    static Config* instance_;
};

看起来没毛病,实际上问题一堆:

  • new 出来的对象谁释放?
  • 多线程同时进 if (instance_ == nullptr) 怎么办?
  • 如果忘了定义 Config::instance_,还会链接报错。
  • 外部虽然不能 new Config(),但如果没有禁用拷贝,仍然可能通过复制制造新对象。

避坑提示

单例模式不是“写一个静态指针”这么简单。它真正要解决的是:创建入口唯一、实例数量唯一、生命周期可控、多线程初始化安全。


二、核心概念:单例模式到底在控制什么

单例模式的核心目的只有一句话:

保证一个类在整个程序运行期间有且仅有一个实例,并提供一个全局访问点。

这句话可以拆成三个动作。

核心支柱 具体手段 解决的问题
禁止外部创建 构造函数设为 private 外部不能 new Singleton(),也不能直接声明对象
禁止复制对象 拷贝构造、赋值运算符 = delete 防止通过复制绕过唯一性
提供唯一入口 static GetInstance() 外部只能从固定入口拿到实例

1. 为什么构造函数必须私有化

如果构造函数是 public,外部可以这样写:

Singleton s1;
Singleton s2;

那就已经不是单例了。

也可以这样写:

auto* p1 = new Singleton();
auto* p2 = new Singleton();

这更失控。

所以第一步一定是:

private:
    Singleton() = default;

意思是:这个类的对象只能由类自己创建,外部没有资格创建。

2. 为什么还要禁用拷贝和赋值

很多人只记得私有化构造函数,却忘了复制也能产生新对象。

比如如果你返回的是对象本身,或者内部某个接口暴露不当,可能出现:

Singleton copy = Singleton::GetInstance();

为了从语义上彻底堵住复制入口,标准写法会加上:

Singleton(const Singleton&)            = delete;
Singleton& operator=(const Singleton&) = delete;

含义很直白:

  • 不允许拷贝构造
  • 不允许赋值复制

如果有人误用,编译期直接报错。

底层思考

单例模式真正怕的不是“你现在没写错”,而是“未来某个调用者能不能绕过你的设计”。= delete 的价值就在这里:把错误用法提前卡死在编译期。

3. 为什么 GetInstance() 必须是静态函数

普通成员函数必须先有对象才能调用:

Singleton obj;
obj.GetInstance();

但单例的矛盾点就在这里:对象不能由外部创建。

所以访问入口必须属于类本身,而不是某个对象:

static Singleton& GetInstance();

调用方式就是:

Singleton::GetInstance();

这就是所谓的“全局访问点”。


三、渐进式演进:从能跑到真正推荐

这一节是重点。我们不直接丢最终答案,而是按真实学习路径一步步升级。

Version 1.0:最朴素的单例实现

第一版代码可以这样写:

#include <iostream>

class Singleton
{
public:
    // 对外提供全局访问点
    static Singleton* GetInstance()
    {
        // 第一次访问时创建对象
        if (instance_ == nullptr)
        {
            instance_ = new Singleton();
        }

        return instance_;
    }

    void SayHello()
    {
        std::cout << "Hello Singleton\n";
    }

private:
    // 构造函数私有化,禁止外部直接创建对象
    Singleton() = default;

private:
    // 保存唯一实例的静态指针
    static Singleton* instance_;
};

// 静态成员变量必须在类外定义
Singleton* Singleton::instance_ = nullptr;

int main()
{
    Singleton::GetInstance()->SayHello();
    return 0;
}

这版代码能跑,也能体现单例的基本思想。

但它不是好实现。

核心逻辑拆解
  1. instance_ 是一个静态指针,所有调用者共享。
  2. 第一次调用 GetInstance() 时,发现 instance_ == nullptr,于是 new Singleton()
  3. 后续调用直接返回之前创建的对象。
  4. 构造函数是 private,外部不能直接创建对象。

看似完整,其实有明显缺陷。

Version 1.0 的问题

问题 1:内存释放不明确

对象是这样创建的:

instance_ = new Singleton();

那它应该在哪里 delete

如果一直不释放,严格来说就是泄漏。虽然很多进程退出时操作系统会回收内存,但这不是一个干净的 C++ 资源管理方式。

如果手动提供 Destroy()

static void Destroy()
{
    delete instance_;
    instance_ = nullptr;
}

又会引入新问题:

  • 谁负责调用?
  • 调用早了,后面模块再访问怎么办?
  • 多线程下一个线程销毁,另一个线程正在使用怎么办?
问题 2:多线程不安全

假设两个线程同时第一次调用:

if (instance_ == nullptr)
{
    instance_ = new Singleton();
}

可能发生这样的时序:

时间点 线程 A 线程 B
1 判断 instance_ == nullptr 成立
2 判断 instance_ == nullptr 成立
3 创建一个 Singleton
4 又创建一个 Singleton

结果就是:单例变双例。

问题 3:没有禁用拷贝和赋值

如果接口稍微改错,或者未来维护者返回对象而不是指针/引用,就可能复制出新实例。

所以单例类必须显式写:

Singleton(const Singleton&)            = delete;
Singleton& operator=(const Singleton&) = delete;

Version 2.0:用锁修复线程安全

既然多线程下 if (instance_ == nullptr) 有问题,那我们可以加锁。

#include <iostream>
#include <mutex>

class Singleton
{
public:
    Singleton(const Singleton&)            = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* GetInstance()
    {
        // 加锁保护初始化过程,避免多个线程同时创建对象
        std::lock_guard<std::mutex> lock(mutex_);

        if (instance_ == nullptr)
        {
            instance_ = new Singleton();
        }

        return instance_;
    }

    static void Destroy()
    {
        std::lock_guard<std::mutex> lock(mutex_);

        delete instance_;
        instance_ = nullptr;
    }

    void SayHello()
    {
        std::cout << "Hello Singleton\n";
    }

private:
    Singleton() = default;
    ~Singleton() = default;

private:
    static Singleton* instance_;
    static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

这版比 1.0 强不少:

  • 构造函数私有化了。
  • 禁用了拷贝和赋值。
  • 初始化过程加锁了。
  • 提供了销毁函数。

但它仍然不是最推荐的现代 C++ 写法。

核心逻辑拆解
  1. std::mutex mutex_ 用来保护共享指针 instance_
  2. std::lock_guard<std::mutex> 利用 RAII 自动加锁、自动解锁。
  3. 多线程同时调用 GetInstance() 时,只有一个线程能进入临界区。
  4. Destroy() 负责释放 new 出来的对象。
这版还有什么不舒服

第一,每次调用 GetInstance() 都要加锁。

即使对象已经创建好了,仍然要进入锁逻辑:

std::lock_guard<std::mutex> lock(mutex_);

如果 GetInstance() 调用频率很高,这就是不必要的开销。

第二,生命周期还是需要人工管理。

你必须记得调用:

Singleton::Destroy();

忘了不干净,早了会出事,多线程下还要考虑并发访问。

第三,代码变复杂了。

手动维护:

  • 静态指针
  • 静态锁
  • 初始化
  • 销毁

这套东西本质上是在和 C++ 运行时抢活干。

避坑提示

能交给语言标准保证的事情,不要自己手写一套复杂机制。单例初始化就是典型例子。


Version 3.0:Meyers’ Singleton,现代 C++ 首选写法

C++ 里最推荐的单例写法是 Meyers’ Singleton。

代码非常短:

#include <iostream>

class Singleton
{
public:
    // 禁止拷贝构造,防止通过复制创建第二个实例
    Singleton(const Singleton&) = delete;

    // 禁止赋值运算,防止把单例状态复制给另一个对象
    Singleton& operator=(const Singleton&) = delete;

    // 全局唯一访问点
    static Singleton& GetInstance()
    {
        // 局部静态变量:第一次执行到这里时初始化
        // C++11 起,局部静态变量初始化由标准保证线程安全
        static Singleton instance;

        // 返回引用,避免空指针,也避免调用者 delete
        return instance;
    }

    void SayHello()
    {
        std::cout << "Hello Meyers Singleton\n";
    }

private:
    // 构造函数私有化,外部不能创建对象
    Singleton() = default;

    // 析构函数私有化,外部不能手动 delete
    // = default 表示使用编译器生成的默认析构函数,不是不析构
    ~Singleton() = default;
};

int main()
{
    Singleton::GetInstance().SayHello();
    return 0;
}

这就是很多 C++ 项目里最常见、最推荐的版本。

核心逻辑拆解

关键只有这一行:

static Singleton instance;

它不是普通局部变量。

虽然写在函数里面,但因为有 static,所以它具有静态存储期

属性 说明
写法位置 函数内部
生命周期 从第一次初始化成功开始,到程序退出
内存区域 静态/全局存储区
初始化时机 第一次执行到这行代码
销毁时机 main() 结束或调用 exit() 后,由 C++ 运行时析构
线程安全 C++11 起标准保证初始化线程安全

这几个点非常重要。

它写在函数里,所以外部访问不到。

它是 static,所以不会随着函数返回销毁。

它第一次调用才创建,所以是懒加载。

它由运行时负责销毁,所以不需要手动 delete

为什么 Meyers’ Singleton 是首选

1. 自动线程安全

C++11 起,标准规定:

局部静态变量的初始化过程是线程安全的。

也就是说,多个线程同时第一次调用 GetInstance() 时,编译器和运行时会保证只有一个线程完成初始化。

你不需要自己写:

std::mutex
std::lock_guard
2. 延迟加载

实例不会在程序启动时立刻创建,而是在第一次调用时创建:

Singleton::GetInstance();

不调用,就不初始化。

这对配置、日志、线程池这类资源很有用。

3. 自动释放

instance 是静态存储期对象。程序退出时,C++ 运行时会自动调用它的析构函数。

所以不需要:

delete instance;

也不需要:

Destroy();
4. 返回引用更安全

推荐返回:

static Singleton& GetInstance()

而不是:

static Singleton* GetInstance()

原因是:

返回方式 特点 风险
返回指针 调用者要用 -> 可能误以为需要判空或 delete
返回引用 调用者用 . 语义更明确:一定存在,不归你释放

单例对象一旦返回,就应该表达“它一定存在,且生命周期由类自己管理”。

引用更符合这个语义。


四、企业级实战:配置文件单例

前面的 SayHello() 只是演示。真实项目里,单例经常用于:

  • 配置管理器
  • 日志系统
  • 线程池
  • 全局资源注册表
  • 硬件设备管理器

下面用配置文件单例举例。

假设项目里用 tomlplusplus 解析 TOML 配置文件,用 LOG_E / LOG_I 打日志。

完整代码如下:

#pragma once

#include <sstream>
#include <string>

#include "spdlog/log_common.hpp"
#include "tomlplusplus/toml.hpp"
#include "utils/Log.h"

namespace config {

class ConfigSingleton
{
public:
    // 禁止拷贝构造,保证不会通过复制得到第二份配置管理器
    ConfigSingleton(const ConfigSingleton&) = delete;

    // 禁止赋值运算,避免单例对象之间状态复制
    ConfigSingleton& operator=(const ConfigSingleton&) = delete;

    // 获取全局唯一配置对象
    static ConfigSingleton& GetInstance()
    {
        // C++11 起线程安全;第一次调用时创建;程序退出时自动析构
        static ConfigSingleton instance;
        return instance;
    }

    // 从 TOML 文件加载配置
    bool LoadFromFile(const std::string& conf_file = "./config/camera.toml")
    {
        try
        {
            // toml::parse_file 可能因为文件不存在、格式错误等原因抛异常
            config_ = toml::parse_file(conf_file);
        }
        catch (const std::exception& e)
        {
            // 捕获异常,记录错误,返回 false 让上层决定是否退出程序
            LOG_E("TomlConfig Load Error: {}", std::string(e.what()));
            return false;
        }

        // 把配置内容转成字符串,便于启动时确认实际加载内容
        std::ostringstream oss;
        oss << config_;
        LOG_I("camera.toml context:{}", oss.str());

        return true;
    }

    // 对外只暴露 const 引用,避免外部随意修改配置表
    [[nodiscard]] const toml::table& Config() const
    {
        return config_;
    }

private:
    // 构造函数私有化,外部不能创建 ConfigSingleton
    ConfigSingleton() = default;

    // 析构函数私有化,外部不能 delete
    // = default 会正常析构成员变量 config_
    ~ConfigSingleton() = default;

private:
    // 保存解析后的 TOML 表
    toml::table config_;
};

}  // namespace config

使用方式

程序启动阶段加载一次:

bool ok = config::ConfigSingleton::GetInstance().LoadFromFile("./config/camera.toml");
if (!ok)
{
    // 根据项目要求决定:退出、使用默认配置、或者进入降级逻辑
    return -1;
}

业务模块里读取配置:

const auto& cfg = config::ConfigSingleton::GetInstance().Config();

auto width  = cfg["camera"]["width"].value_or(1920);
auto height = cfg["camera"]["height"].value_or(1080);
auto fps    = cfg["camera"]["fps"].value_or(30);

为什么这里返回 const toml::table&

配置对象对外暴露时,推荐这样写:

[[nodiscard]] const toml::table& Config() const
{
    return config_;
}

原因有三个:

写法 结果
返回 toml::table 会复制整张配置表,没必要
返回 toml::table& 外部可以随便改配置,风险大
返回 const toml::table& 不复制,并且外部不能直接修改

[[nodiscard]] 的作用是提醒调用者:这个返回值你大概率不应该忽略。

如果有人写:

config::ConfigSingleton::GetInstance().Config();

但没有使用返回值,编译器可以给出提醒。

LoadFromFile 为什么返回 bool

配置加载失败是很现实的情况:

  • 文件路径写错
  • 文件不存在
  • TOML 语法错误
  • 权限不足
  • 部署时漏拷贝配置文件

所以 LoadFromFile() 不应该假装一定成功。

推荐这样设计:

bool LoadFromFile(const std::string& conf_file)

失败时:

return false;

让上层决定怎么处理。

避坑提示

配置加载失败不要静默吞掉。启动阶段直接打印明确错误,比运行半小时之后某个模块拿到默认值再炸要好排查得多。


五、避坑指南:这些坑初学者特别容易踩

坑 1:以为 = default 等于“不释放”

看到这行代码:

~Singleton() = default;

很多人会误解:

默认析构是不是代表什么都不做?那成员变量会不会不释放?

答案很明确:

= default 不是“不析构”,而是让编译器生成默认析构函数。

默认析构函数会做该做的事情:

  • 调用成员变量的析构函数。
  • 调用基类的析构函数。
  • 按 C++ 对象模型正常销毁对象。

比如:

class Demo
{
private:
    std::string name_;
    std::vector<int> data_;
};

即使你写:

~Demo() = default;

name_data_ 也会被正常析构。

真正危险的是你手写了错误的析构逻辑,或者用裸指针管理资源却忘了释放。

坑 2:局部静态变量不是栈变量

这行代码特别容易误导人:

static Singleton instance;

因为它写在函数里面,有人会以为它在栈上。

但它不是。

普通局部变量才在栈上:

Singleton instance;

加了 static 后,它具有静态存储期:

static Singleton instance;

对比一下:

变量形式 内存/生命周期特点
普通局部变量 通常在栈上,函数返回后销毁
局部静态变量 静态存储期,第一次执行到定义处初始化,程序退出时销毁
new 创建对象 堆上,需要手动 delete 或智能指针管理
全局变量 程序启动阶段初始化,程序退出时销毁

所以 Meyers’ Singleton 既不是栈对象,也不是堆对象,而是局部作用域里的静态存储期对象

坑 3:只私有化构造函数,不禁用拷贝

错误写法:

class Singleton
{
public:
    static Singleton& GetInstance()
    {
        static Singleton instance;
        return instance;
    }

private:
    Singleton() = default;
};

这版代码少了:

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

正确写法:

class Singleton
{
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

private:
    Singleton() = default;
    ~Singleton() = default;
};

单例模式要堵住的是所有创建第二个实例的入口。

构造函数只是其中一个入口,复制也是入口。

坑 4:返回指针导致调用者误解所有权

不推荐:

static Singleton* GetInstance()
{
    static Singleton instance;
    return &instance;
}

虽然这也能工作,但指针容易给调用者错误暗示:

  • 这个指针会不会是空?
  • 我要不要判空?
  • 我要不要释放?

推荐:

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

引用表达的语义更干净:

这个对象一定存在,你只管用,不要管释放。

坑 5:多个单例之间互相依赖

比如:

Logger::GetInstance().Info("config loaded");
Config::GetInstance().LoadFromFile();

如果 ConfigSingleton 的析构函数里又写日志:

~ConfigSingleton()
{
    Logger::GetInstance().Info("destroy config");
}

程序退出时就可能遇到析构顺序问题。

原因是:不同静态对象之间的析构顺序并不总是符合你的直觉。

如果 Logger 已经析构了,ConfigSingleton 析构时再用日志,就可能出问题。

避坑提示

单例之间尽量不要在析构阶段互相调用。尤其是日志、配置、线程池这种全局设施,退出阶段的依赖关系要非常克制。

坑 6:把单例当万能全局变量

单例本质上是“受控的全局状态”。

它适合管理真正全局唯一的资源:

适合使用单例 不适合使用单例
配置管理器 普通业务对象
日志系统 用户订单对象
线程池 临时计算上下文
硬件设备管理器 每个请求独立的数据

如果一个对象并不要求全局唯一,就不要硬做成单例。

否则后果就是:

  • 模块耦合变强。
  • 单元测试变难。
  • 状态污染难排查。
  • 代码越来越像到处乱飞的全局变量。

坑 7:单元测试不友好

单例持有全局状态,测试时很容易出现这种情况:

TEST(ConfigTest, LoadA)
{
    ConfigSingleton::GetInstance().LoadFromFile("a.toml");
}

TEST(ConfigTest, LoadB)
{
    ConfigSingleton::GetInstance().LoadFromFile("b.toml");
}

如果两个测试共享同一个单例对象,就可能互相影响。

解决思路有几种:

方案 适用场景 代价
提供测试专用 Reset() 小项目、内部工具 破坏单例纯粹性
测试进程隔离 状态复杂、要求稳定 执行成本更高
依赖注入替代单例 对可测试性要求高 架构稍复杂
只把无状态服务做单例 工具类、日志类 适用范围有限

实际项目里要根据场景取舍。

如果是配置中心这种启动时加载一次、运行期只读的对象,单例可以接受。

如果是业务状态频繁变化的对象,就要谨慎。


六、几种单例实现方式对比

把常见写法放在一张表里,对比会更清楚。

实现方式 线程安全 是否懒加载 是否需要手动释放 推荐度 说明
裸指针 + new 不推荐 早期写法,容易泄漏和竞态
裸指针 + mutex 一般 能用但代码复杂,生命周期麻烦
双重检查锁 DCLP 可以做到 通常需要 谨慎 细节多,现代 C++ 没必要优先选
饿汉式静态对象 可用 程序启动即创建,适合轻量对象
Meyers’ Singleton C++11 起安全 首选 简洁、懒加载、自动析构

饿汉式是什么

饿汉式就是程序启动时提前创建对象。

class Singleton
{
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton& GetInstance()
    {
        return instance_;
    }

private:
    Singleton() = default;
    ~Singleton() = default;

private:
    static Singleton instance_;
};

Singleton Singleton::instance_;

它的优点是简单,初始化也天然只发生一次。

缺点是:不管你用不用,程序启动时都会创建它。

如果对象很轻量,比如一个无状态工具类,问题不大。

如果对象初始化很重,比如加载配置、连接数据库、启动线程池,就不适合无脑饿汉式。

双重检查锁为什么不优先推荐

很多文章会写这种:

if (instance_ == nullptr)
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (instance_ == nullptr)
    {
        instance_ = new Singleton();
    }
}

这叫双重检查锁。

它的目的很明确:

  • 外层 if:避免每次都加锁。
  • 内层 if:避免多个线程排队进入锁后重复创建。

但这类写法涉及内存模型、指令重排、对象构造可见性等细节。

现代 C++ 里,如果没有非常特殊的理由,直接用 Meyers’ Singleton 就够了。

核心结论

面试可以理解双重检查锁,项目里优先写 Meyers’ Singleton。少写一堆并发细节,就少一堆隐藏 bug。


七、最终推荐模板

如果你只是想在项目里快速写一个标准、干净、现代 C++ 风格的单例,可以直接套这个模板:

class Singleton
{
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

private:
    Singleton() = default;
    ~Singleton() = default;
};

如果类里有业务方法,就加在 public 区域:

class Logger
{
public:
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    static Logger& GetInstance()
    {
        static Logger instance;
        return instance;
    }

    void Info(const std::string& message)
    {
        // 写日志逻辑
    }

private:
    Logger() = default;
    ~Logger() = default;
};

调用方式:

Logger::GetInstance().Info("program started");

记住这个形状就够用了。


八、复习心法:一张表带走

问题 正确理解
单例模式解决什么问题 保证某个类全程序只有一个实例,并提供统一访问入口
构造函数为什么私有 防止外部直接创建对象
为什么要 = delete 拷贝和赋值 防止通过复制创建第二个实例
GetInstance() 为什么是 static 没有对象时也能通过类名访问
推荐返回指针还是引用 推荐引用,语义更明确,不需要判空和释放
static Singleton instance 在哪里 静态/全局存储区,不是栈,不是堆
什么时候创建 第一次执行到定义处时创建
什么时候销毁 程序退出时由 C++ 运行时自动析构
= default 是不是不析构 不是,会生成正常的默认析构函数
C++11 后局部静态变量线程安全吗 初始化过程线程安全
最推荐写法 Meyers’ Singleton
最大风险 滥用导致全局状态泛滥、测试困难、模块耦合

再压缩成一句话:

现代 C++ 写单例,优先用函数内局部静态变量;构造私有,拷贝删除,返回引用,让语言标准替你处理线程安全和生命周期。


九、最后给初学者的判断标准

写单例之前,先问自己三个问题:

  1. 这个对象是否真的全程序只应该有一个?
  2. 它是否代表某种全局资源,比如配置、日志、线程池、设备管理器?
  3. 它持有的状态会不会让测试和模块边界变得混乱?

如果三个问题都想清楚了,再用单例。

如果只是为了“哪里都能访问方便”,那大概率是在给未来埋坑。

单例不是不能用,而是要用在刀刃上。

真正成熟的写法不是把所有东西都做成 Singleton,而是只让那些天然全局唯一、生命周期贯穿程序、访问入口需要统一管理的对象成为 Singleton。

这才是单例模式的正确打开方式。

更多推荐