6、单例模式:模式动机与定义、懒汉式与饿汉式实现、线程安全实现(C++11 magic static)、优缺点分析

6.1 模式动机——为什么需要单例?

说实话,单例模式可能是设计模式里最「接地气」的一个。你想想看,一个系统里有些对象只需要一个实例就够了——比如日志管理器、配置中心、线程池、数据库连接池。如果每个模块都 new 一个出来,不仅浪费内存,还会出现数据不一致的问题。

我在项目中遇到过这样一个坑:一个后台服务,日志模块被多个线程同时调用。一开始没做单例,结果每个线程各自创建了一个日志实例,写文件时互相覆盖,日志全乱套了。排查了半天才发现是实例不唯一的问题。从那以后,我对单例模式就格外上心。

说白了,单例模式的核心动机就两条:

  • 控制实例数量——确保一个类只有一个对象
  • 提供全局访问点——让任何地方都能拿到这个唯一的实例

6.2 模式定义

单例模式的定义其实很简单:保证一个类仅有一个实例,并提供一个访问它的全局访问点。嗯,就这两句话。但实现起来,细节可不少。

我个人的习惯是,先画一张图把整体逻辑理清楚,再动手写代码。下面这张 SVG 图展示了单例模式的核心结构:

单例模式核心结构 Singleton - static instance: Singleton* - Singleton() + static getInstance(): Singleton* + someMethod(): void 自引用 Client Client 所有客户端通过 getInstance() 获取同一个实例

6.3 饿汉式实现——简单粗暴

饿汉式,顾名思义,就是「很饿」,程序一启动就把实例创建好。不管后面用不用,先 new 出来再说。

// 饿汉式单例
class Singleton {
public:
    static Singleton* getInstance() {
        return &instance;
    }
    
    void doSomething() {
        // 业务逻辑
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton instance;  // 类加载时就初始化
};

// 在源文件中定义
Singleton Singleton::instance;

饿汉式的优点很明显:实现简单,天生线程安全。因为实例在 main 函数执行前就创建好了,不存在多线程竞争的问题。

但缺点也很致命:如果这个单例对象很重,或者程序根本没用它,那就白白浪费了资源。我记得有一次接手一个老项目,里面有个配置管理器用的饿汉式,启动时加载了 200MB 的配置文件。结果大部分功能根本不需要这个配置,白白拖慢了启动速度。

注意:饿汉式不适合「资源密集型」或「启动时不确定是否使用」的场景。如果你不确定,建议用懒汉式。

6.4 懒汉式实现——按需加载

懒汉式就聪明多了:什么时候用,什么时候创建。第一次调用 getInstance() 时才 new 对象。

// 懒汉式单例(非线程安全)
class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;

这个版本有个大问题——线程不安全。两个线程同时调用 getInstance(),可能都发现 instance 是 nullptr,然后各自 new 了一个对象。这就违背了单例的初衷。

我曾经在一个高并发的网络服务里用过这个版本,上线第一天就出了 bug。两个线程同时创建了日志实例,结果日志文件被交替写入,内容全乱了。嗯,从那以后我再也不敢用裸指针的懒汉式了。

6.5 线程安全实现——C++11 magic static

C++11 引入了一个「魔法」特性:函数局部静态变量的初始化是线程安全的。标准规定,如果多个线程同时首次访问一个静态局部变量,只有一个线程会执行初始化,其他线程会阻塞等待。

利用这个特性,我们可以写出最优雅、最高效的单例实现:

// C++11 magic static 单例(线程安全)
class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // 线程安全的局部静态变量
        return instance;
    }
    
    void doSomething() {
        // 业务逻辑
    }
    
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

这个版本的好处太多了:

  • 线程安全——C++11 标准保证,不需要加锁
  • 懒加载——第一次调用时才创建
  • 代码简洁——没有指针,没有锁,没有 double-check
  • 自动析构——程序结束时自动销毁,没有内存泄漏
我的建议:在 C++11 及以后的版本中,优先使用 magic static 实现单例。这是目前最安全、最简洁的方式。我最近五年的项目全部用的这个方案,没出过任何问题。

6.6 三种实现方式对比

特性 饿汉式 懒汉式(裸指针) Magic Static
线程安全 ✅ 天生安全 ❌ 不安全 ✅ C++11 保证
懒加载 ❌ 启动即创建 ✅ 按需创建 ✅ 按需创建
代码复杂度
性能开销 无运行时开销 需加锁(若安全) 首次调用有开销
自动析构 ❌ 需手动管理
适用场景 轻量对象,必定使用 不推荐使用 绝大多数场景

6.7 优缺点分析

优点:

  • 实例唯一性——从根源上保证只有一个对象
  • 全局访问——任何地方都能拿到实例,方便
  • 延迟初始化——懒汉式可以节省资源
  • 避免频繁创建销毁——适合重量级对象

缺点:

  • 违反单一职责——既要管理实例,又要做业务逻辑
  • 不利于扩展——如果以后需要多个实例,改动很大
  • 隐藏依赖关系——全局访问点让模块间耦合变高
  • 测试困难——单例的全局状态会影响单元测试

核心总结:单例模式是一把双刃剑。用好了,能优雅地管理全局资源;用不好,会让代码变得难以维护。我个人建议:能用依赖注入就别用单例,非要用单例就选 magic static。记住,单例不是银弹,它只是一个工具。


公众号:蓝海资料掘金营,微信deep3321