【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.unlock()
之前抛出异常,锁将不会被释放。 - 逻辑复杂时忘记解锁: 程序员可能忘记调用
unlock()
,导致死锁。
- 异常未处理时锁未释放: 如果在
使用 std::lock_guard
的好处
- 简化代码: 避免显式调用
lock()
和unlock()
。 - 异常安全: 无论代码是否异常终止,
std::lock_guard
都会确保锁被释放。 - 减少出错: RAII 自动管理资源,降低因疏忽造成的死锁风险。
第三种方法:双重检查锁定,在保证线程安全的同时提高性能。
加锁确保多线程环境下只创建一个实例,并且用两个if判断来提高效率。
第四种方法:饿汉式,实例在程序加载时创建,无法延迟加载。
【注】:
- 私有构造函数不能直接在类外调用。
- 但在静态成员变量初始化时,类的构造函数调用是受编译器特殊处理的,允许在类外完成静态成员初始化。
第五种方法:静态内部类实现,支持延迟加载,线程安全,推荐使用。
改进建议(C++11 起推荐方式)
在 C++11 标准中,可以使用局部静态变量实现单例模式,实现懒加载(实例在首次调用 getInstance
时创建),避免手动管理锁和指针:
优点:
static
局部变量在 C++11 中是线程安全的,简化了代码。- 不需要手动管理指针,避免内存泄漏的风险。
- 相比上述方法5,扩展性较差。
-----------------------------------------------------------------------------------------------------------------------------------------------------
局部静态变量的特点:
- 普通局部变量:
- 每次调用函数时,局部变量都会被重新创建,离开函数作用域时销毁。
- 变量的值在下一次调用时不会保留。
static
局部变量:- 只在函数中定义,但其生命周期贯穿程序的整个运行过程。
- 它只会被初始化一次,后续函数调用时会保留上一次的值。
- 作用域: 依然是函数的局部作用域,函数外无法访问它。
- 生命周期: 在程序的整个生命周期内存在,存储在全局/静态数据区。