1 单例模式的基础知识
单例模式( Singleton Pattern )是一种常见的设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这个模式非常有用,尤其是在需要频繁访问某个对象,而且该对象的创建和销毁代价很大时。通过单例模式,可以确保系统中只有一个对象实例,从而节省系统资源。
1.1 单例模式的概念
单例模式的核心目的是限制一个类只能创建一个对象,从而提供一个全局的唯一访问点。该模式的概念包括以下几个关键点:
唯一性
单例模式保证一个类只有一个实例存在。无论尝试多少次创建该类的实例,都只会返回同一个对象实例。这是通过控制类的实例化过程来实现的,通常是在类内部实现一个静态私有变量来保存该类的唯一实例。
自行创建实例
单例模式中的类负责自行创建其唯一实例。这通常在类加载时或第一次使用时完成,具体取决于实现方式(如懒汉式或饿汉式)。一旦实例被创建,就可以通过全局访问点来访问它。
全局访问点
单例模式提供一个全局访问点,通常是一个静态的公共方法,用于获取类的唯一实例。其他对象可以通过这个方法访问单例对象,而无需自行创建。这使得单例对象可以在整个系统中被共享和访问。
注意:单例模式虽然具有许多优点,但在使用时也需要注意线程安全和资源释放等问题。在多线程环境下,需要采取额外的同步措施来确保单例的唯一性。同时,在程序结束时,也需要确保正确释放单例对象所占用的资源。
1.2 单例模式的适用场景
单例模式的适用场景如下:
全局访问点
需要提供一个全局唯一的访问点来访问某个资源或状态时,可以使用单例模式。例如,配置文件管理、日志记录、应用程序的计数器等。
资源限制
当资源创建和销毁的代价很大,或者资源数量有限时,可以使用单例模式来限制资源的数量。例如,数据库连接池的设计通常使用单例模式,因为数据库连接是一种昂贵的资源,通过维护一个连接池可以减少连接的创建和销毁次数,提高性能。
共享资源
当需要共享某个资源或状态时,可以使用单例模式。例如,多线程的线程池设计通常使用单例模式,因为线程池需要方便对池中的线程进行控制,确保线程的安全使用和复用。
系统状态管理
当需要管理系统的全局状态时,可以使用单例模式。例如,操作系统的文件系统通常只有一个实例,可以通过单例模式来实现。
2 单例模式的常用类型及实现
根据实现方式和线程安全性的考虑,单例模式的常用类型可以分为懒汉式单例模式、饿汉式单例模式、线程安全的懒汉式单例模式这三种。
2.1 懒汉式单例模式
懒汉式单例模式是一种单例模式的实现方式,其特点是类加载时并不创建实例,而是在第一次使用时才创建实例。这种方式也被称为延迟初始化( Lazy Initialization )。
懒汉式单例模式通常包括以下步骤:
(1)定义一个私有静态变量:用于保存单例对象的引用。
(2)定义私有构造函数:确保外部无法通过 new 关键字创建实例。
(3)提供一个公共的静态方法:用于获取单例对象的引用。如果单例对象尚未创建,则在此方法中创建实例并返回;如果实例已经存在,则直接返回该实例。
在 C++ 中实现普通的懒汉式单例模式(非线程安全版本)相对简单,因为不需要考虑多线程环境下的同步问题。但是,这样的实现在多线程环境下是不安全的,因为多个线程可能同时创建单例的多个实例。下面是一个非线程安全的懒汉式单例模式类:
cpp
#include <iostream>
class LazySingleton
{
public:
// 获取单例实例
static LazySingleton& getInstance()
{
if (nullptr == instance)
{
instance = new LazySingleton();
}
return *instance;
}
// 私有构造函数,防止外部创建实例
private:
LazySingleton() {}
// 拷贝构造函数和赋值操作符需要被删除,以防止复制实例
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
// 静态成员变量,保存单例实例的指针
static LazySingleton* instance;
};
// 初始化静态成员变量
LazySingleton* LazySingleton::instance = nullptr;
在这个示例中, LazySingleton 类有一个静态成员变量 instance ,初始化为 nullptr 。 getInstance() 方法首先检查 instance 是否为 nullptr ,如果是,则创建一个新的 LazySingleton 实例。由于 getInstance() 方法没有使用任何同步机制,因此它在多线程环境下是不安全的。
使用上面的单例可以创建一个全局的对象,如下为样例代码:
cpp
int main()
{
// 获取单例实例
LazySingleton& singleton1 = LazySingleton::getInstance();
LazySingleton& singleton2 = LazySingleton::getInstance();
// 检查是否真的是同一个实例
if (&singleton1 == &singleton2) {
std::cout << "singleton1 and singleton2 are the same instance." << std::endl;
}
else {
std::cout << "singleton1 and singleton2 are different instances." << std::endl;
}
return 0;
}
上面代码的输出为:
singleton1 and singleton2 are the same instance.
2.2 饿汉式单例模式
饿汉式单例模式的特点是类加载时就完成了单例实例的初始化,因此它是线程安全的,不需要额外的同步机制。在C++中实现饿汉式单例模式相对简单,如下为样例代码:
cpp
#include <iostream>
class HungrySingleton
{
public:
// 获取单例实例
static HungrySingleton& getInstance()
{
// 由于实例在类加载时就已初始化,因此无需检查或锁定
return instance;
}
// 私有构造函数,防止外部创建实例
private:
HungrySingleton() {}
// 拷贝构造函数和赋值操作符需要被删除,以防止复制实例
HungrySingleton(const HungrySingleton&) = delete;
HungrySingleton& operator=(const HungrySingleton&) = delete;
// 静态成员变量,保存单例实例
static HungrySingleton instance;
};
// 在类定义外部初始化静态成员变量
HungrySingleton HungrySingleton::instance;
在这个示例中,HungrySingleton 类有一个静态成员变量 instance ,它在全局作用域中被初始化。由于 instance 在程序开始执行之前就已经被初始化,因此它是线程安全的。 getInstance() 方法直接返回这个已初始化的实例,不需要任何同步机制。
调用上面饿汉式单例类的对象也很简单:
cpp
int main()
{
// 获取单例实例
HungrySingleton& singleton1 = HungrySingleton::getInstance();
HungrySingleton& singleton2 = HungrySingleton::getInstance();
// 检查是否真的是同一个实例
if (&singleton1 == &singleton2)
{
std::cout << "singleton1 and singleton2 are the same instance." << std::endl;
}
else
{
std::cout << "singleton1 and singleton2 are different instances." << std::endl;
}
return 0;
}
上面代码的输出为:
singleton1 and singleton2 are the same instance.
注意:由于饿汉式单例模式在类加载时就完成了实例的初始化,它可能不适用于所有情况。特别是当单例的初始化很耗时,或者依赖于在运行时才能确定的值时,懒汉式单例模式可能更合适。然而,在不需要延迟初始化,并且希望避免多线程同步开销的情况下,饿汉式单例模式是一个很好的选择。
2.3 线程安全的懒汉式单例模式
线程安全懒汉式单例模式是懒汉式单例模式的一种改进,该模式用于在多线程环境下实现单例对象的延迟初始化,同时保持线程安全。该模式可以通过两次检查实例是否已经被创建来避免不必要的同步开销。如下为具体实现:
cpp
#include <iostream>
#include <mutex>
class ThreadSafeSingleton
{
public:
// 静态方法,用于获取单例对象的引用
static ThreadSafeSingleton& getInstance()
{
// 第一次检查,如果实例已经存在,则直接返回
if (instance == nullptr)
{
// 创建一个互斥锁对象
static std::mutex mtx;
// 加锁
std::lock_guard<std::mutex> lock(mtx);
// 第二次检查,确保在锁的保护下再次检查实例是否存在
if (instance == nullptr)
{
// 如果实例不存在,则创建实例
instance.reset(new ThreadSafeSingleton());
}
}
return *instance;
}
// 私有的构造函数,确保外部无法直接创建实例
ThreadSafeSingleton() {}
// 私有的析构函数,确保外部无法删除实例
~ThreadSafeSingleton() {}
// 私有的拷贝构造函数,防止实例被复制
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
// 私有的赋值操作符,防止实例被赋值
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
private:
// 静态成员变量,使用智能指针来管理单例实例
static std::unique_ptr<ThreadSafeSingleton> instance;
};
// 初始化静态成员变量
std::unique_ptr<ThreadSafeSingleton> ThreadSafeSingleton::instance = nullptr;
在上面代码中,getInstance() 方法首先检查 instance 是否为 nullptr 。如果是,则创建一个互斥锁对象 mtx ,并使用 std::lock_guard 来自动管理锁的生命周期。在锁的保护下,再次检查 instance 是否为 nullptr 。如果仍然是 nullptr ,则创建单例实例。由于 std::lock_guard 会在其析构时自动释放锁,因此不需要手动解锁。与前面懒汉式单例模式的实现相比较,这里的单例对象使用了智能指针,这样在该对象的生命周期结束后,不需要手动删除它。
如下是对上面定义类的使用:
cpp
int main()
{
// 获取单例实例
ThreadSafeSingleton& singleton1 = ThreadSafeSingleton::getInstance();
ThreadSafeSingleton& singleton2 = ThreadSafeSingleton::getInstance();
// 检查是否真的是同一个实例
if (&singleton1 == &singleton2)
{
std::cout << "singleton1 and singleton2 are the same instance." << std::endl;
}
else {
std::cout << "singleton1 and singleton2 are different instances." << std::endl;
}
return 0;
}
上面代码的输出为:
singleton1 and singleton2 are the same instance.
线程安全的懒汉式单例模式能够减少不必要的同步开销,因为只有在第一次访问单例时才需要加锁。然而,需要注意的是, C++11 之前的标准并不支持在静态局部变量的初始化中使用线程安全的初始化,因此在这些标准下,上面代码中的双重检查锁定可能不会正常工作。 C++11 及更高版本的标准提供了对静态局部变量初始化的线程安全保证,因此上述代码在 C++11 及更高版本中是安全的。
除了上面的实现方式,还可以利用 C++11 的一些新特性(如 std::call_once 和 std::once_flag )来简化线程安全的实现。如下为样例代码:
cpp
class ThreadSafeSingleton
{
public:
// 获取单例实例
static ThreadSafeSingleton& getInstance()
{
// std::call_once保证下面的lambda只执行一次
std::call_once(instanceInitFlag, []() {
instance.reset(new ThreadSafeSingleton());
});
return *instance;
}
// 私有构造函数,防止外部创建实例
ThreadSafeSingleton() { }
// 拷贝构造函数和赋值操作符需要被删除,以防止复制实例
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
private:
// 静态成员变量,保存单例实例的指针
static std::unique_ptr<ThreadSafeSingleton> instance;
// std::once_flag用于配合std::call_once保证线程安全
static std::once_flag instanceInitFlag;
};
// 初始化静态成员变量
std::unique_ptr<ThreadSafeSingleton> ThreadSafeSingleton::instance = nullptr;
std::once_flag ThreadSafeSingleton::instanceInitFlag;
在这个示例中, LazySingleton 类有一个静态成员变量 instance ,它是一个指向 LazySingleton 类型的指针,初始化为 nullptr 。还有一个静态成员变量 instanceInitFlag ,它是一个 std::once_flag 类型,用于确保初始化过程只执行一次。
getInstance() 方法使用 std::call_once 来确保初始化代码块只执行一次,即使多个线程同时调用 getInstance() 。初始化代码块创建一个新的 LazySingleton 实例,并将其地址赋给 instance 智能指针。
3 单例模式的应用实例(全局的日志记录类)
在 C++ 中,可以使用单例模式来设计一个全局的日志记录类。这个类将负责记录应用程序中的所有日志信息,并且确保在整个应用程序的生命周期中只有一个日志记录器实例存在。下面是一个简单的示例代码,展示了如何使用单例模式来实现一个全局的日志记录类:
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <mutex>
// 日志记录类
class Logger
{
public:
// 获取日志记录器的单例实例
static Logger& getInstance()
{
// std::call_once保证下面的 lambda 只执行一次
std::call_once(instanceInitFlag, []() {
instance.reset(new Logger());
});
return *instance;
}
// 记录日志信息
void log(const std::string& message)
{
// 打开日志文件
std::ofstream logFile("log.txt", std::ios_base::app);
if (!logFile.is_open())
{
std::cerr << "Failed to open log file!" << std::endl;
return;
}
// 写入日志信息
logFile << message << std::endl;
logFile.close();
}
private:
// 私有的构造函数,确保外部无法直接创建实例
Logger() {}
// 私有的静态成员变量,持有单例对象
static std::unique_ptr<Logger> instance;
// std::once_flag用于配合std::call_once保证线程安全
static std::once_flag instanceInitFlag;
};
// 初始化静态成员变量
std::unique_ptr<Logger> Logger::instance = nullptr;
std::once_flag Logger::instanceInitFlag;
// 全局函数,用于记录日志
void logMessage(const std::string& message)
{
Logger& logger = Logger::getInstance();
logger.log(message);
}
在这个示例中,Logger 类是一个线程安全的懒汉模式单例类,它使用了一个私有的静态成员变量 instance 来保存单例实例。getInstance() 方法提供了获取单例实例的全局访问点。
log() 方法用于记录日志信息。在这个简单的示例中,它只是简单地将日志信息追加到一个名为 log.txt 的文件中。当然,在实际应用中,可能需要实现更复杂的日志记录逻辑,比如日志级别控制、日志格式化、异步日志记录等。
logMessage() 是一个全局函数,它简化了从外部调用日志记录器的方法。可以在任何需要记录日志的地方调用这个函数。
如下是一个具体的调用:
cpp
int main()
{
// 在不同的地方调用全局的日志记录函数
logMessage("hello logger");
// 可以在其他模块或函数中调用logMessage()来记录日志
return 0;
}
4 单例模式的优缺点
单例模式具有一些优点和缺点,下面分别进行介绍:
优点
(1)节省内存:单例模式确保一个类只有一个实例,这有助于节省内存,特别是当需要频繁创建和销毁对象时。
(2)提高性能:由于单例模式避免了频繁创建和销毁对象,因此可以提高系统的性能。
(3)简化代码:单例模式简化了代码,因为不需要编写创建和管理实例的代码。
(4)控制资源访问:单例模式可以控制对资源的访问,确保只有一个实例可以访问资源,从而避免资源的多重占用。
缺点
(1)违反单一职责原则:单例模式通常将多个功能集中在一个类中,这可能导致类的职责过重,违反了单一职责原则。
(2)扩展困难:单例模式没有抽象层,因此扩展困难。如果要扩展单例类,通常需要修改原来的代码,这违背了开闭原则。
(3)调试困难:在调试过程中,如果单例中的代码没有执行完,则无法模拟生成一个新的对象,这可能给调试带来困难。
总体而言,单例模式具有节省内存、提高性能等优点,但也存在违反单一职责原则、扩展困难、调试困难等缺点。在使用时需要根据具体情况权衡其优缺点,谨慎选择是否使用单例模式。