【C/C++】单例模式

第一种方法:不使用锁,在多线程环境下不能正常工作

    

【注】:静态成员变量的初始化必须在类的外部进行,并且需要通过 类作用域解析运算符::)来指定变量所属的类。

 

第二种方法:加锁确保多线程环境下只创建一个实例,但加锁是一个非常耗时的操作(每次访问都加锁,性能较低)。

加锁操作:

使用了 std::lock_guard<std::mutex> 来管理互斥锁(std::mutex)。

std::lock_guard<std::mutex> 是一个 RAII(资源获取即初始化)类型的工具:

    • 当创建一个 std::lock_guard 对象时,它会自动锁住给定的 std::mutex
    • std::lock_guard 对象超出其作用域(代码块结束)时,会自动调用 std::mutex::unlock(),释放锁。

这就使得代码更加简洁且安全,无需显式调用 unlock(),避免了手动释放锁可能出现的错误(例如异常或忘记解锁)。

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

    • 构造了一个 std::lock_guard 对象 lock,并将互斥锁 mutex 作为参数传入。
    • 构造时,自动调用 mutex.lock(),将互斥锁加锁。
  • 作用域内:

    • lock 的生命周期内,mutex 会保持加锁状态。
  • 作用域结束:

    • lock 对象超出作用域时(代码块结束),std::lock_guard 的析构函数会被调用。
    • 析构函数自动调用 mutex.unlock(),释放锁。

如果手动加锁和解锁

如果没有使用 std::lock_guard,你需要手动调用 lock()unlock(),如下:

mutex.lock(); // 手动加锁
if (instance == nullptr) {
  instance = new Singleton3(); // 创建实例
}
mutex.unlock(); // 手动解锁

但这种方式容易出错,例如:

    • 异常未处理时锁未释放: 如果在 mutex.unlock() 之前抛出异常,锁将不会被释放。
    • 逻辑复杂时忘记解锁: 程序员可能忘记调用 unlock(),导致死锁。

使用 std::lock_guard 的好处

  • 简化代码: 避免显式调用 lock()unlock()
  • 异常安全: 无论代码是否异常终止,std::lock_guard 都会确保锁被释放。
  • 减少出错: RAII 自动管理资源,降低因疏忽造成的死锁风险。

 

第三种方法:双重检查锁定,在保证线程安全的同时提高性能。

                     加锁确保多线程环境下只创建一个实例,并且用两个if判断来提高效率。

 

第四种方法:饿汉式,实例在程序加载时创建,无法延迟加载。

【注】:

  • 私有构造函数不能直接在类外调用。
  • 但在静态成员变量初始化时,类的构造函数调用是受编译器特殊处理的,允许在类外完成静态成员初始化。

 

第五种方法:静态内部类实现,支持延迟加载,线程安全,推荐使用。

 

改进建议(C++11 起推荐方式)

在 C++11 标准中,可以使用局部静态变量实现单例模式,实现懒加载(实例在首次调用 getInstance 时创建),避免手动管理锁和指针:

class Singleton {
  private:
    Singleton() {}
  public:
    static Singleton& getInstance() {
         static Singleton instance; // 局部静态变量,线程安全
         return instance;
    }
};

优点:

  • static 局部变量在 C++11 中是线程安全的,简化了代码。
  • 不需要手动管理指针,避免内存泄漏的风险。
  • 相比上述方法5,扩展性较差。

-----------------------------------------------------------------------------------------------------------------------------------------------------

 局部静态变量的特点:

  • 普通局部变量:
    • 每次调用函数时,局部变量都会被重新创建,离开函数作用域时销毁。
    • 变量的值在下一次调用时不会保留。
  • static 局部变量:
    • 只在函数中定义,但其生命周期贯穿程序的整个运行过程。
    • 它只会被初始化一次,后续函数调用时会保留上一次的值。
    • 作用域: 依然是函数的局部作用域,函数外无法访问它。
    • 生命周期: 在程序的整个生命周期内存在,存储在全局/静态数据区。

 

posted @ 2024-12-01 17:42  朝槿yys  阅读(15)  评论(0编辑  收藏  举报