文章目录
- 一、引言
- 二、懒汉模式
- 三、饿汉模式
- 四、C++11 的线程安全单例
- 五、与其他模式的关系
- 六、总结
一、引言
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
在一个项目中,全局范围内,某个类的实例有且仅有一个,通过这个唯一实例向其他模块提供数据的全局访问,这种模式就叫单例模式。单例模式的典型应用就是任务队列。
在使用单例模式时,核心是确保类的实例只能有一个,就像“独生子女”一样,不能有其他兄弟姐妹。为了实现这一点,需要采取以下措施:
- 构造函数私有化:将构造函数(拷贝构造和构造)设置为私有,使得外部无法直接创建实例。这样在类内部可以控制只创建一次实例。
- 由于使用者在类外部不能使用构造函数,所以在类内部创建的这个唯一的对象必须是静态的,这样就可以通过类名来访问了,为了不破坏类的封装,我们都会把这个静态对象的访问权限设置为私有的。
- 在类中只有它的静态成员函数才能访问其静态成员变量,所以可以给这个单例类提供一个静态函数用于得到这个静态的单例对象。
- 静态实例:由于构造函数是私有的,需要在类内部通过一个静态变量来创建唯一的实例,并且使其私有化,以保证外部无法访问或修改。
- 提供静态访问方法:通过一个静态成员函数,外部可以访问这个静态的单例对象。这种方式既能保证实例的唯一性,又保持了封装性。
- 禁止或私有化拷贝和赋值:通过将拷贝构造函数和拷贝赋值操作符删除(使用
= delete
),防止通过拷贝或赋值创建新的实例。这样可以杜绝实例化多个对象的可能性。通过这些措施,我们可以确保类只能有一个实例,从而实现了一个线程安全且高效的单例模式。
单例模式的主要目的是确保一个类在程序运行期间只有一个实例,并提供一个全局访问点来访问该实例。单例模式可以通过懒汉式(Lazy Initialization)和饿汉式(Eager Initialization)两种方式实现。下面分别介绍这两种实现方法。
二、懒汉模式
懒汉式单例是指在需要时才创建单例对象。它的优点是延迟加载,不会在程序启动时就占用内存。缺点是需要考虑多线程环境下的安全问题。
// 懒汉模式
class Singleton
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
// 获取单例实例的静态方法
static Singleton* getInstance() {
// 双重检查锁定,确保多线程下的线程安全性
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() = default; // 私有化构造函数
static Singleton* instance; // 单例实例指针
static std::mutex mutex_; // 互斥锁,保证线程安全
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
在以上代码中,getInstance()
方法通过双重检查锁定(Double-Checked Locking)确保在多线程环境下的安全性,同时避免每次获取实例时都加锁。
双重检查锁定是一种用于懒汉式单例模式的优化技术,主要目的是在多线程环境下创建单例对象时,既保证线程安全性,又避免不必要的加锁操作,提高性能。
- 为什么需要双重检查锁定?
当我们在多线程环境中创建单例时,如果不进行适当的同步控制,可能会导致多个线程同时创建实例,从而产生多个对象,违背了单例模式的初衷。为了解决这个问题,我们通常会对获取实例的过程加锁,但是简单的加锁会带来性能问题。
假设我们没有使用双重检查锁定,而是简单地在
getInstance
方法中对整个创建过程加锁:static Singleton* getInstance() { std::lock_guard<std::mutex> lock(mutex_); if (instance == nullptr) { instance = new Singleton(); } return instance; }
上面的代码可以保证线程安全性,但每次调用
getInstance
方法时都会加锁,即使实例已经被创建。加锁和解锁是一个耗时操作,会影响性能。
- 双重检查锁定是如何优化的?
双重检查锁定通过在加锁前后分别检查实例是否为空,减少了不必要的加锁操作。具体步骤如下:
static Singleton* getInstance() { // 第一次检查(不加锁) if (instance == nullptr) { // 加锁,防止多个线程同时创建实例 std::lock_guard<std::mutex> lock(mutex_); // 第二次检查(加锁后) if (instance == nullptr) { instance = new Singleton(); } } return instance; }
- 第一次检查:在加锁前检查
instance
是否为空。如果已经有实例了,就直接返回,无需加锁。- 加锁:如果
instance
为空,说明实例还没有被创建,进行加锁操作,确保只有一个线程能够进入创建实例的代码块。- 第二次检查:加锁后,再次检查
instance
是否为空。这样做是因为在第一个线程加锁前,其他线程可能已经创建了实例。只有在instance
仍然为空时,才会创建新实例。
- 双重检查锁定的工作原理
性能优化:大多数情况下,
getInstance
方法在实例已经存在时,不会进行加锁操作。这显著提高了性能,尤其是在实例已经被创建后,其他线程只需要进行第一次检查即可返回实例。线程安全:在实例未创建时,通过加锁保证了只有一个线程可以创建实例,其他线程会被阻塞在锁外,确保了单例对象只被创建一次。
三、饿汉模式
饿汉模式就是在类加载的时候立刻进行实例化,这样就得到了一个唯一的可用对象。关于这个饿汉模式的类的定义如下:
饿汉式单例是在类加载时就创建实例。它的优点是不需要考虑多线程同步问题,因为在类加载时就完成了初始化;缺点是无论是否使用都会占用内存。也就是没有线程安全问题。
// 饿汉模式
class Singleton
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton& obj) = delete;
static Singleton* getInstance()
{
return instance;
}
private:
Singleton() = default;
static Singleton* instance;
};
// 静态成员初始化放到类外部处理
Singleton* Singleton::instance = new Singleton;
int main()
{
Singleton* obj = Singleton::getInstance();
}
在这个例子中,Singleton
类的实例在程序启动时就被创建好了,不需要加锁。每次调用 getInstance()
时,都会返回相同的实例。
注:类的静态成员变量在使用之前必须在类的外部进行初始化才能使用。
在调用getInstance()
函数获取单例对象的时候,如果在单线程情况下是没有什么问题的,如果是多个线程,调用这个函数去访问单例对象就有问题了。假设有三个线程同时执行了getInstance()
函数,在这个函数内部每个线程都会new
出一个实例对象。此时,这个任务队列类的实例对象不是一个而是3个,很显然这与单例模式的定义是相悖的。
四、C++11 的线程安全单例
在实现懒汉模式的单例的时候,相较于双重检查锁定模式有一种更简单的实现方法并且不会出现线程安全问题,那就是使用静态局部局部对象。
自C++11起,可以利用静态局部变量的特性实现线程安全的单例模式。这种方式既简单又安全:
#include <iostream>
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证局部静态变量的线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void show() {
std::cout << "Singleton instance: " << this << std::endl;
}
private:
Singleton() = default;
};
int main() {
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
s1.show();
s2.show();
return 0;
}
在这个实现中,getInstance()
方法中的静态局部变量 instance
只会在第一次调用时初始化,并且C++11保证其线程安全。因此,这种方式在多线程环境中也是安全的,而且代码简洁易读。
这种方式不仅简化了代码,还保证了线程安全性,是在现代C++中推荐使用的单例模式实现方式。
- 双重检查锁定主要用于在多线程环境下的懒汉式单例,避免重复创建实例,同时减少不必要的加锁操作。
- 它通过两次检查(一次在加锁前,一次在加锁后),确保只有在实例未创建时才进行加锁和实例化,从而提高性能。
- 在C++11及以后,可以直接使用静态局部变量的特性来实现线程安全的单例,而不需要使用双重检查锁定。
五、与其他模式的关系
- 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
- 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
六、总结
- 懒汉式单例适用于需要延迟加载实例的场景,但在多线程环境下需要加锁。
- 饿汉式单例简单且不需要考虑线程安全,但在不需要时也会占用内存。
- C++11静态局部变量单例实现方式最为简洁,并且在多线程环境下是安全的,是推荐使用的方法。
懒汉模式的缺点是在创建实例对象的时候有安全问题,但这样可以减少内存的浪费(如果用不到就不去申请内存了)。饿汉模式则相反,在我们不需要这个实例对象的时候,它已经被创建出来,占用了一块内存。对于现在的计算机而言,内存容量都是足够大的,这个缺陷可以被无视。