设计模式 单例模式(singleton pattern)包括 懒汉式(Lazy Initialization)、饿汉式(Eager Initialization)、可以将普通类变成单例类的模版和代码实践
flyfish
2014-12-16
2024-07-19
单例模式(singleton pattern)
设计模式(design pattern)-》 创建型模式(Creational Patterns)-》Singleton
单例模式是一种在软件开发中常用的设计模式,它的主要目标是确保一个类只有一个实例,并且提供一个全局访问点来获取这个实例。这在很多情况下非常有用,比如当一个系统需要共享资源或配置信息时,确保这些信息只有一份副本可以避免不必要的重复和冲突。
实现单例模式的关键点在于:
控制实例化过程:通常,我们会把类的构造函数设为私有,这样外部代码就不能直接创建这个类的新实例。这是因为我们希望所有对这个类的实例化请求都通过一个特定的静态方法进行。
提供静态方法获取实例:类中会包含一个静态方法(通常命名为getInstance()),这个方法负责检查是否已经有实例存在。如果没有,它会创建一个新实例;如果有,它会返回已存在的那个实例。这样,不管有多少次调用getInstance()方法,只会创建并返回同一个实例。
线程安全性:在多线程环境中,如果多个线程同时调用getInstance()方法,可能会导致创建多个实例,破坏单例的特性。为了避免这种情况,我们需要在判断实例是否存在以及创建实例的过程中加上同步机制,确保同一时刻只有一个线程可以执行这部分代码。
编写单例模式前需要了解的基础知识
在C++中,编译器会为每个类自动生成一些默认的特殊成员函数(Special Member Functions),这些函数包括:
-
默认构造函数(Default Constructor) :
如果类没有用户定义的构造函数,编译器会生成一个默认的无参构造函数。 -
析构函数(Destructor) :
如果类没有用户定义的析构函数,编译器会生成一个默认的析构函数。 -
拷贝构造函数(Copy Constructor) :
如果类没有用户定义的拷贝构造函数,编译器会生成一个默认的拷贝构造函数,该构造函数执行浅拷贝。 -
拷贝赋值运算符(Copy Assignment Operator) :
如果类没有用户定义的拷贝赋值运算符,编译器会生成一个默认的拷贝赋值运算符,该运算符执行浅拷贝。 -
移动构造函数(Move Constructor) (C++11引入):
如果类没有用户定义的移动构造函数,且类的某些成员支持移动操作,编译器会生成一个默认的移动构造函数。 -
移动赋值运算符(Move Assignment Operator) (C++11引入):
如果类没有用户定义的移动赋值运算符,且类的某些成员支持移动操作,编译器会生成一个默认的移动赋值运算符。
#include <iostream>
#include <utility> // for std::move
class MyClass {
public:
// 默认构造函数(Default Constructor)
// 作用:创建一个未初始化的对象。
MyClass() : data(new int(0)) {
std::cout << "Default Constructor called\n";
}
// 析构函数(Destructor)
// 作用:释放资源,防止内存泄漏。
~MyClass() {
std::cout << "Destructor called\n";
delete data;
}
// 拷贝构造函数(Copy Constructor)
// 作用:创建一个新对象,并将现有对象的值复制到新对象中。
MyClass(const MyClass& other) : data(new int(*other.data)) {
std::cout << "Copy Constructor called\n";
}
// 拷贝赋值运算符(Copy Assignment Operator)
// 作用:将现有对象的值复制到另一个现有对象中。
MyClass& operator=(const MyClass& other) {
std::cout << "Copy Assignment Operator called\n";
if (this != &other) {
*data = *other.data;
}
return *this;
}
// 移动构造函数(Move Constructor)
// 作用:将资源从一个对象转移到另一个对象中,而不是复制资源。
MyClass(MyClass&& other) noexcept : data(other.data) {
std::cout << "Move Constructor called\n";
other.data = nullptr; // 断开原对象与资源的联系
}
// 移动赋值运算符(Move Assignment Operator)
// 作用:将资源从一个对象转移到另一个对象中,而不是复制资源。
MyClass& operator=(MyClass&& other) noexcept {
std::cout << "Move Assignment Operator called\n";
if (this != &other) {
delete data; // 释放当前对象的资源
data = other.data; // 转移资源
other.data = nullptr; // 断开原对象与资源的联系
}
return *this;
}
// 用于展示内部数据的函数
void showData() const {
std::cout << "Data: " << *data << std::endl;
}
private:
int* data;
};
int main() {
MyClass obj1; // 调用默认构造函数
obj1.showData();
MyClass obj2 = obj1; // 调用拷贝构造函数
obj2.showData();
MyClass obj3; // 调用默认构造函数
obj3 = obj1; // 调用拷贝赋值运算符
obj3.showData();
MyClass obj4 = std::move(obj1); // 调用移动构造函数
obj4.showData();
MyClass obj5; // 调用默认构造函数
obj5 = std::move(obj2); // 调用移动赋值运算符
obj5.showData();
return 0;
}
在单例模式中,为了确保类的唯一实例,通常需要特别处理这些特殊成员函数。以下是对这6个特殊成员函数在单例模式中的处理方式:
下面展示如何处理这些特殊成员函数,主要是防止任何形式的复制或移动操作。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取唯一实例的静态方法
static Singleton& getInstance() {
static Singleton instance; // 局部静态变量,线程安全,C++11保证
return instance;
}
// 禁用拷贝构造函数和拷贝赋值运算符 使用 `delete` 关键字禁用,以防止复制单例对象。
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 禁用移动构造函数和移动赋值运算符 使用 `delete` 关键字禁用,以防止移动单例对象。
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 示例方法
void doSomething() {
std::cout << "Singleton instance doing something\n";
}
private:
// 私有构造函数和析构函数 构造函数和析构函数设为私有,防止外部创建或销毁实例。
Singleton() {
std::cout << "Singleton constructor called\n";
}
~Singleton() {
std::cout << "Singleton destructor called\n";
}
};
int main() {
Singleton& instance = Singleton::getInstance();
instance.doSomething();
// 以下代码将导致编译错误
// Singleton copy = instance;
// Singleton anotherInstance;
// anotherInstance = instance;
return 0;
}
单例模式可以通过两种主要的方式来实现(完整的两份代码)
懒汉式(Lazy Initialization):这意味着单例的实例会在第一次被请求时创建。这种方式的优点是延迟加载,节省了内存资源,但缺点是初始化过程可能会稍微慢一些,因为需要在首次请求时创建实例。
饿汉式(Eager Initialization):在这种情况下,单例的实例会在类加载时就已经创建好了。这样做的好处是,一旦类加载,单例就可以立即使用,无需等待初始化,但缺点是如果这个单例最终并没有被使用,那么预先创建的实例就浪费了内存。
简而言之,单例模式就是确保一个类只有一个实例,并且这个实例可以通过一个全局访问点获取。这种模式在处理全局配置、数据库连接池、日志记录器等场景中非常有用。在实现时,需要注意线程安全性和初始化时机的选择。
下面分别说这两种模式
饿汉式单例实现:实例在程序开始时即创建,使用局部静态变量保证线程安全,适用于实例创建开销小且确定需要的情况。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取单例实例的静态方法
static Singleton& getInstance() {
static Singleton instance; // 局部静态变量,保证线程安全 在饿汉式单例模式中,单例对象在类加载时就创建了
return instance;
}
// 示例方法
void showMessage() {
std::cout << "Hello, Singleton Pattern (Eager Initialization)!" << std::endl;
}
private:
// 私有构造函数,防止外部实例化
Singleton() {
std::cout << "Singleton Instance Created" << std::endl;
}
// 私有析构函数,防止外部删除实例
~Singleton() {}
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 禁止移动构造和赋值操作
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
};
int main() {
Singleton& singleton = Singleton::getInstance();
singleton.showMessage();
return 0;
}
懒汉式单例实现:实例在第一次使用时才创建,使用std::call_once和std::once_flag保证线程安全,适用于实例创建开销大且不确定是否需要的情况。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取单例实例的静态方法 在懒汉式单例模式中,单例对象在第一次调用getInstance时创建
static Singleton& getInstance() {
std::call_once(initInstanceFlag, &Singleton::initSingleton);
return *instance;
}
// 示例方法
void showMessage() {
std::cout << "Hello, Singleton Pattern (Lazy Initialization)!" << std::endl;
}
private:
// 私有构造函数,防止外部实例化
Singleton() {
std::cout << "Singleton Instance Created" << std::endl;
}
// 私有析构函数,防止外部删除实例
~Singleton() {}
// 禁止拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 禁止移动构造和赋值操作
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
// 初始化单例实例的方法
static void initSingleton() {
instance = new Singleton();
}
static Singleton* instance;
static std::once_flag initInstanceFlag;
};
// 静态成员变量初始化
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initInstanceFlag;
int main() {
Singleton& singleton = Singleton::getInstance();
singleton.showMessage();
return 0;
}
为什么代码中使用局部静态变量(Meyers’ Singleton)实现线程安全的单例模式
在单例模式中,通过锁来确保线程安全确实会引入一些性能问题,尤其是在高并发的情况下。为了提升性能,可以使用以下几种方法来实现线程安全的单例模式,同时避免频繁获取锁的问题:
-
双重检查锁定(Double-Checked Locking)
-
局部静态变量(Meyers’ Singleton)
双重检查锁定(Double-Checked Locking):通过两次检查实例是否为空,减少了锁开销,确保了线程安全,但代码相对复杂。
局部静态变量(Meyers’ Singleton):利用 C++11 的特性,实现了线程安全的单例模式,代码简洁高效,推荐在现代 C++ 中使用。
这两种技术都解决了单例模式在多线程环境中的线程安全问题,但局部静态变量的实现方式更为简洁和直接,是现代 C++ 推荐的单例模式实现方法。
双重检查锁定(Double-Checked Locking)
双重检查锁定是一种优化方法,通过减少锁的使用次数来提高性能。在这种方法中,第一次检查不需要获取锁,只有在第一次检查通过后才会获取锁并进行第二次检查。这种方式可以显著减少获取锁的次数,从而提高性能。
在 getInstance 方法中,先检查实例是否为空,如果为空,则加锁并再次检查,如果仍为空,则创建实例。
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取单例实例的静态方法
static Singleton& getInstance() {
if (instance == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_); // 获取锁
if (instance == nullptr) { // 第二次检查
instance = new Singleton();
}
}
return *instance;
}
void showMessage() {
std::cout << "Hello, Singleton Pattern with Double-Checked Locking!" << std::endl;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
static Singleton* instance;
static std::mutex mutex_;
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
int main() {
Singleton& singleton = Singleton::getInstance();
singleton.showMessage();
return 0;
}
局部静态变量(Meyers’ Singleton)
使用局部静态变量是一种更简单且高效的方法。在C++11中,局部静态变量的初始化是线程安全的。因此,这种方法既简单又能确保线程安全性。
在 getInstance 方法中,使用局部静态变量,该变量在函数第一次被调用时初始化,并且在 C++11 标准下是线程安全的。
#include <iostream>
class Singleton {
public:
// 获取单例实例的静态方法
static Singleton& getInstance() {
static Singleton instance; // 局部静态变量,C++11保证线程安全
return instance;
}
void showMessage() {
std::cout << "Hello, Singleton Pattern with Meyers' Singleton!" << std::endl;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
};
int main() {
Singleton& singleton = Singleton::getInstance();
singleton.showMessage();
return 0;
}
选择谁
-
双重检查锁定 :这种方法适用于在一些特殊情况下需要手动管理内存分配和释放的场景。双重检查锁定在保证线程安全的前提下,尽量减少了锁的开销。
-
局部静态变量(Meyers’ Singleton) :这是最推荐的方法,尤其是在C++11及以后的版本中。它的实现简单,性能高效,且天然线程安全。
一般情况下,推荐使用局部静态变量(Meyers’ Singleton) ,因为它不仅实现简单且性能优越,并且现代C++编译器对局部静态变量的初始化提供了良好的线程安全保障。
实现模板单例模式(完整代码)
可以将普通类变成单例类
#include <iostream>
// 单例模板类
template <typename T>
class Singleton {
public:
// 获取唯一实例的静态方法
static T& getInstance() {
static T instance; // 饿汉式单例,在程序启动时创建实例
return instance;
}
// 禁用拷贝构造函数和拷贝赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 禁用移动构造函数和移动赋值运算符
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
protected:
// 保护构造函数和析构函数,防止外部创建和销毁实例
Singleton() = default;
~Singleton() = default;
};
// 示例类
class MyClass {
public:
void doSomething() {
std::cout << "MyClass instance doing something\n";
}
};
// 使用单例模板包装示例类
using MyClassSingleton = Singleton<MyClass>;
int main() {
MyClass& instance = MyClassSingleton::getInstance();
instance.doSomething();
// 以下代码将导致编译错误
// MyClass copy = instance;
// MyClass anotherInstance;
// anotherInstance = instance;
return 0;
}
单例模板类 Singleton:
template 定义模板。
static T& getInstance() 方法返回类 T 的唯一实例,使用静态局部变量实现饿汉式单例模式。
拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符均被禁用,以防止复制或移动实例。
构造函数和析构函数设为保护级别,防止外部创建或销毁实例。
示例类 MyClass:
一个普通的类,包含一个方法 doSomething,用于演示单例模式。
将示例类包装成单例类:
使用 using 关键字将 Singleton 命名为 MyClassSingleton