设计模式:单例模式

1. 什么是单例模式

单例模式是一种创建型 设计模式,确保一个类只有一个实例 ,并提供全局访问点。它常用于需要控制资源共享的场景,如数据库连接、日志系统或配置管理。

单例模式的核心特点

  • 唯一实例:类只能创建一个对象实例。
  • 全局访问:通过静态方法或属性提供全局访问入口。
  • 私有构造:防止外部直接通过构造函数实例化。

2. 单例模式常见的实现方式有哪些?

2.1 懒汉式(线程不安全)

在首次调用时创建实例,适合非多线程环境。

cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {} // 私有构造函数

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
 

缺点:多线程环境下可能创建多个实例。

2.2 懒汉式(线程安全,加锁)

cpp 复制代码
#include <mutex>
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
 

通过互斥锁保证线程安全,但锁开销影响性能。

2.3 饿汉式(线程安全)

cpp 复制代码
class Singleton {
private:
    static Singleton instance;
    Singleton() {}

public:
    static Singleton* getInstance() {
        return &instance;
    }
};
Singleton Singleton::instance;
 

程序启动时即初始化实例,无需考虑线程安全问题,但可能浪费资源(实际可以忽略啦,比较推荐)。

2.4 Meyers' Singleton(静态局部变量)

arduino 复制代码
class Singleton {
private:
    Singleton() {}

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

C++11后静态局部变量初始化是线程安全的,既延迟加载又无需显式加锁。

2.5 双重检查锁定(DCLP)

cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
};
 

减少锁的竞争,但需注意内存屏障问题(C++11后可用atomic解决)。

2.如何保证线程安全

在C++中实现单例模式时,通常会将构造函数、拷贝构造函数和赋值运算符设为私有,并通过静态方法获取唯一实例。基础的非线程安全实现如下(懒汉式):

cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    Singleton() {} // 私有构造函数
    Singleton(const Singleton&) = delete; // 禁用拷贝
    Singleton& operator=(const Singleton&) = delete; // 禁用赋值

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
 

在多线程环境下,多个线程可能同时进入if (instance == nullptr)判断,导致创建多个实例,违背单例模式原则。

懒汉式线程不安全的原因

当线程A和线程B同时调用getInstance()时:

  1. 两者都可能检测到instance == nullptr
  2. 两者都会执行new Singleton()
  3. 最终导致内存泄漏和实例不唯一

线程安全的改进方案

方案1:加锁(双重检查锁定)
cpp 复制代码
class Singleton {
private:
    static Singleton* instance;
    static std::mutex mtx;
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
 
方案2:局部静态变量(C++11起线程安全)
cpp 复制代码
class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

在C++11之前,局部静态变量的初始化在多线程环境下可能存在竞态条件,导致多次初始化或未定义行为。C++11标准明确规定了局部静态变量的线程安全初始化机制。

线程安全初始化的实现原理:

C++11要求编译器在局部静态变量初始化时插入线程同步代码。通常采用**双重检查锁定模式(Double-Checked Locking Pattern)**实现:

  • 第一次检查避免每次调用都加锁
  • 加锁后第二次检查确保只有一个线程执行初始化
  • 使用内存屏障保证可见性

假如我们这么使用局部变量

cpp 复制代码
void foo() {
    static MyClass instance;
    // 使用instance
}
 

编译器生成的等效线程安全代码可能类似:

cpp 复制代码
void foo() {
    static MyClass* instance = nullptr;
    static std::atomic_flag flag;
    
    if (!instance) {
        std::lock_guard guard(lock);
        if (!instance) {
            instance = new MyClass();
        }
    }
    // 使用*instance
}
 

局部静态变量线程安全初始化适用于:

  • 单例模式的实现(Meyers' Singleton)
  • 需要延迟初始化的全局资源
  • 线程安全的惰性初始化

需要注意:

  • 析构顺序仍然是未定义的
  • 不同编译单元的静态变量初始化顺序问题仍然存在
  • 性能敏感场景应考虑其他方案
替代方案比较

对于需要更高性能的场景,可以考虑:

  • 使用函数外的静态变量(牺牲延迟初始化特性)
  • 显式使用std::call_once
  • 依赖项注入模式
cpp 复制代码
// 使用std::call_once的示例
void foo() {
    static std::once_flag flag;
    static MyClass* instance;
    
    std::call_once(flag, [](){
        instance = new MyClass();
    });
    // 使用*instance
}
 
方案3:原子操作(C++11)
cpp 复制代码
#include <atomic>
class Singleton {
private:
    static std::atomic<Singleton*> instance;
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    static Singleton* getInstance() {
        Singleton* tmp = instance.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(mtx);
            tmp = instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton();
                std::atomic_thread_fence(std::memory_order_release);
                instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }
};
std::atomic<Singleton*> Singleton::instance;
std::mutex Singleton::mtx;
 

最佳实践建议

对于现代C++项目,推荐使用方案2的局部静态变量实现:

  1. 代码简洁
  2. C++11标准保证线程安全
  3. 不需要手动管理内存
  4. 按需初始化(真正懒汉式)

早期C++版本或需要更精细控制时,可采用方案1的双重检查锁定模式。方案3适用于需要极致性能的场景,但代码复杂度较高。