1. 什么是单例模式?
单例模式是一种创建型设计模式,其核心思想是确保一个类在任何情况下都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。
可以把它想象成一个国家的总统或者一个学校的校长,在整个系统运行期间,这个角色只能有一个人担任。无论你从哪个部门、哪个流程去“找校长”,最终找到的都是同一个人。
为了在 C++ 中实现这一点,通常需要满足三个关键条件:
- 私有的构造函数:为了防止外部代码通过
new 操作符随意创建类的实例。
- 一个私有的、静态的、指向本类实例的指针或对象:这是存放那个唯一实例的地方。
- 一个公有的、静态的、用于获取实例的方法:这是全局唯一的访问点,负责创建并返回那个唯一的实例。
2. 为什么需要单例模式?
单例模式主要用于解决那些“全局唯一”且需要被频繁共享访问的资源或服务。常见的应用场景包括:
- 日志记录器(Logger):整个应用程序通常只需要一个日志记录器,所有模块都通过它来写入日志文件。
- 配置管理器(Configuration Manager):读取和管理应用的配置信息(如数据库连接字符串、API 密钥等),这些信息在整个应用中是共享的。
- 数据库连接池(Database Connection Pool):管理数据库连接,避免频繁地创建和销毁连接,整个应用共享一个连接池。
- 硬件接口访问:比如访问打印机、串口等,通常需要一个统一的管理器来避免冲突。
使用单例模式可以带来以下好处:
- 资源节约:避免了重复创建重量级对象带来的开销。
- 数据一致性:确保所有部分访问的是同一份数据或状态。
- 全局访问:提供了一个方便的全局访问点,简化了代码。
3. 如何实现单例模式?(C++ 实现的演进)
在 C++ 中,单例的实现有多种方式,并且随着 C++ 标准的演进,最佳实践也在发生变化。下面我们来看一下这个演进过程。
版本一:懒汉式(Lazy Singleton)- 基础但线程不安全
“懒汉”指的是只有在第一次被请求时,实例才会被创建,而不是在程序启动时就创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <iostream>
class Singleton { private: Singleton() { std::cout << "Singleton instance created." << std::endl; }
static Singleton* instance_;
public: static Singleton* getInstance() { if (instance_ == nullptr) { instance_ = new Singleton(); } return instance_; }
Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
void showMessage() { std::cout << "Hello from Singleton!" << std::endl; } };
Singleton* Singleton::instance_ = nullptr;
int main() { Singleton* s1 = Singleton::getInstance(); Singleton* s2 = Singleton::getInstance();
if (s1 == s2) { std::cout << "s1 and s2 are the same instance." << std::endl; }
s1->showMessage(); return 0; }
|
- 问题:
- 线程不安全:在多线程环境下,如果两个线程同时进入
if (instance_ == nullptr) 判断,并且都判断为 true,那么它们都会创建一个实例,这就破坏了单例的原则。这被称为竞态条件(Race Condition)。
- 内存泄漏:
new 出来的实例没有被 delete,会导致内存泄漏。虽然可以设计一个 destroy 方法或者使用智能指针,但这会增加复杂性。
版本二:懒汉式 + 锁(Thread-Safe Lazy Singleton)
为了解决线程安全问题,最直接的方法就是加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream> #include <mutex>
class Singleton { private: Singleton() { std::cout << "Singleton instance created." << std::endl; } static Singleton* instance_; static std::mutex mutex_;
public: static Singleton* getInstance() { std::lock_guard<std::mutex> lock(mutex_); if (instance_ == nullptr) { instance_ = new Singleton(); } return instance_; }
Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };
Singleton* Singleton::instance_ = nullptr; std::mutex Singleton::mutex_;
|
- 优点:通过
std::mutex 保证了线程安全。
- 问题:
- 性能开销:每次调用
getInstance() 都需要加锁和解锁,即使实例已经被创建。当实例创建后,这个锁其实是多余的,会影响高并发场景下的性能。
- 内存泄漏问题依然存在。
版本三:双重检查锁定(Double-Checked Locking Pattern, DCLP)
为了解决版本二的性能问题,人们想出了 DCLP。其思想是:先检查一次实例是否存在,如果不存在再加锁,加锁后再检查一次(防止其他线程在等待锁期间已经创建了实例)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
#include <iostream> #include <mutex> #include <atomic>
class Singleton { private: Singleton() { std::cout << "Singleton instance created." << std::endl; } static std::atomic<Singleton*> instance_; static std::mutex mutex_;
public: static Singleton* getInstance() { Singleton* tmp = instance_.load(std::memory_order_relaxed); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(mutex_); tmp = instance_.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton(); instance_.store(tmp, std::memory_order_release); } } return tmp; } };
std::atomic<Singleton*> Singleton::instance_{nullptr}; std::mutex Singleton::mutex_;
|
- 问题:DCLP 的实现在 C++11 之前是不可靠的,因为存在指令重排(Memory Reordering)问题,可能导致一个线程拿到一个尚未完全构造好的对象。在 C++11 及以后,通过
std::atomic 和正确的内存序(memory order)可以正确实现,但它非常复杂、难以理解且容易出错。我们有更好的选择。
版本四:Meyers’ Singleton(C++11 最佳实践)
这是 C++ 之父 Scott Meyers 提出的方法,利用了 C++11 标准中对 静态局部变量(Static Local Variable) 初始化的保证。
C++11 标准规定:在一个函数或块内部的 static 变量的初始化是线程安全的。它只会被初始化一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream>
class Singleton { private: Singleton() { std::cout << "Singleton instance created." << std::endl; }
public: Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() { static Singleton instance; return instance; }
void showMessage() { std::cout << "Hello from Singleton! Address: " << this << std::endl; } };
int main() { Singleton& s1 = Singleton::getInstance(); Singleton& s2 = Singleton::getInstance();
s1.showMessage(); s2.showMessage();
if (&s1 == &s2) { std::cout << "s1 and s2 are the same instance." << std::endl; }
return 0; }
|
- 优点:
- 线程安全:C++11 标准保证了
static Singleton instance; 这行代码的执行是线程安全的。
- 简洁优雅:代码量最少,逻辑最清晰。
- 无内存泄漏:
instance 是一个栈上的静态对象,其生命周期由程序管理,程序结束时会自动析构,无需手动 delete。
- 懒汉式加载:只有在第一次调用
getInstance() 时才会创建实例。
这是现代 C++ 中实现单例模式的首选方案。
版本五:饿汉式(Eager Singleton)
“饿汉”指的是在程序启动时(main 函数执行前),实例就已经被创建好了,不管你用不用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream>
class Singleton { private: Singleton() { std::cout << "Singleton instance created at startup." << std::endl; }
static Singleton instance_;
public: Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() { return instance_; } };
Singleton Singleton::instance_;
int main() { std::cout << "Main function started." << std::endl; Singleton& s1 = Singleton::getInstance(); Singleton& s2 = Singleton::getInstance();
if (&s1 == &s2) { std::cout << "s1 and s2 are the same instance." << std::endl; } return 0; }
|
- 优点:
- 天然线程安全:因为实例在多线程代码开始执行之前就已经创建好了,不存在竞态条件。
- 实现简单。
- 缺点:
- 启动耗时:如果单例的构造函数非常耗时,会拖慢程序的启动速度。
- 资源浪费:如果程序从头到尾都没有使用这个单例,那么它的创建就成了一种资源浪费。
4. 单例模式的优缺点
优点
- 保证唯一实例:核心价值,确保了某些类只有一个对象。
- 全局访问点:提供了一个方便的全局访问点,简化了对象间的调用。
- 延迟初始化:懒汉式实现可以做到只在需要时才创建实例,节约资源。
缺点
- 违反单一职责原则(SRP):一个类既要负责其核心业务逻辑,又要负责管理自己的生命周期(创建、销毁),职责过重。
- 引入全局状态:单例本质上是全局变量的变种,会引入全局状态,使得代码的依赖关系变得不清晰。一个函数依赖了某个单例,但从函数签名上看不出来。
- 难以进行单元测试:由于全局状态和紧耦合,对依赖单例的模块进行单元测试变得非常困难。很难用一个模拟的(Mock)对象来替换单例。
- 扩展性差:如果未来需要这个类有多个实例,修改会非常困难。
5. 一个完整的现代 C++ 示例 (Logger)
下面我们使用 Meyers’ Singleton 实现一个简单的日志记录器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <iostream> #include <fstream> #include <string> #include <mutex> #include <chrono> #include <iomanip>
class Logger { public: static Logger& getInstance() { static Logger instance; return instance; }
Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete;
void log(const std::string& message) { std::lock_guard<std::mutex> lock(mutex_);
auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now);
log_file_ << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X") << " - " << message << std::endl; }
private: Logger() { log_file_.open("application.log", std::ios::out | std::ios::app); if (!log_file_.is_open()) { std::cerr << "Error: Could not open log file!" << std::endl; } else { log("Logger instance created."); } }
~Logger() { if (log_file_.is_open()) { log("Logger instance destroyed."); log_file_.close(); } }
std::ofstream log_file_; std::mutex mutex_; };
#include <thread> void worker_thread(int id) { Logger::getInstance().log("Thread " + std::to_string(id) + " started."); Logger::getInstance().log("Thread " + std::to_string(id) + " finished."); }
int main() { Logger::getInstance().log("Main function started.");
std::thread t1(worker_thread, 1); std::thread t2(worker_thread, 2);
t1.join(); t2.join();
Logger::getInstance().log("Main function finished.");
return 0; }
|
运行这段代码后,你会看到一个 application.log 文件,里面的日志记录了主函数和两个线程的活动,并且所有日志都是通过同一个 Logger 实例写入的。
6. 总结与建议
- 单例模式是一个强大但需要谨慎使用的工具。 它能有效解决全局唯一资源的管理问题。
- 在现代 C++ (C++11及以后) 中,强烈推荐使用 Meyers’ Singleton(基于静态局部变量的实现),因为它最简洁、安全、高效。
- 在使用单例模式前,请三思:是否真的需要一个全局唯一的实例?有没有其他方式可以替代,比如依赖注入(Dependency Injection)?依赖注入通常能提供更好的可测试性和更松的耦合。
- 如果决定使用,请确保你的单例类是线程安全的,尤其是在其成员函数会修改内部状态时(如上面的
log 方法)。