我们编程的时候,有时候会不知道从何下手,学习设计模式为我们提供思路,相应的特性,带入相应的设计模式,会为我们省下不少的精力。
今天我们来了解一个——单例模式
什么是单例模式
单例模式是一种创建型设计模式,其核心目的是确保类只有一个实例,并提供一个全局访问点来访问该实例。这意味着在整个应用程序的生命周期中,只能存在一个特定类的实例。
单例模式通常用于那些需要管理共享资源或全局状态的情况,例如配置信息、数据库连接、日志记录器等。通过单例模式,可以确保系统中的某个类只有一个实例,从而避免了资源重复消耗和状态不一致的问题。
单例模式的一般实现方式包括:
懒汉式(Lazy Initialization): 在首次请求时才创建实例。这种方式在多线程环境下需要考虑线程安全性。
饿汉式(Eager Initialization): 在类加载时就创建实例。这种方式简单直接,但可能会导致资源浪费,因为实例可能永远都不会被使用到。
双重检查锁定(Double-Checked Locking): 结合了懒汉式和饿汉式的优点,既实现了延迟加载又保证了线程安全。
静态内部类: 利用类加载机制保证线程安全,在需要时才加载内部类,从而实现延迟加载。
枚举(Enum): 利用枚举类型的特性,保证实例的唯一性和线程安全性。
懒汉式
懒汉式单例模式会在第一次需要时才实例化对象:下面我们使用C++来实现一下大概逻辑:
//单例模式——懒汉式
class OneInstance
{
public:
static OneInstance* getInstance() //用于获取唯一实例
{
if (instance == nullptr)
{
instance = new OneInstance();
}
return instance;
}
private:
static OneInstance* instance; //唯一实例
OneInstance()
{
}
};
OneInstance* OneInstance::instance = nullptr;
饿汉式
是在类加载时就立即初始化单例对象:
class OneInstance
{
public:
static OneInstance& getInstance() //得到唯一实例
{
return instance;
}
private:
static OneInstance instance; //初始化
OneInstance()
{
}
};
OneInstance OneInstance::instance; //一个实例
懒汉式的线程安全问题
懒汉式单例模式在线程安全方面存在问题是由于它的实例化过程并非原子操作。在多线程环境下,当多个线程同时检测到单例实例尚未创建时,可能会导致多个线程同时进入创建实例的代码块,从而生成多个实例,违背了单例模式只允许存在一个实例的原则。
具体来说,假设有如下非线程安全的懒汉式单例实现:
class OneInstance
{
public:
static OneInstance* getInstance() //用于获取唯一实例
{
if (instance == nullptr)
{
instance = new OneInstance();
}
return instance;
}
private:
static OneInstance* instance; //唯一实例
OneInstance()
{
}
};
OneInstance* OneInstance::instance = nullptr;
在多线程环境下,当两个线程A和B几乎同时到达getInstance
方法,并发现instance
为null时,可能会发生以下情况:
- 线程A进入if条件判断内部,准备创建实例。
- 线程B也同样进入了if条件判断内部,因为它还没有看到线程A对
instance
的赋值。- 线程A执行
new Singleton()
,开始实例化对象。- 线程B同样执行
new Singleton()
,也开始实例化对象。- 结果是生成了两个Singleton实例,违反了单例原则。
为了解决这个问题,需要在创建单例实例的代码上添加适当的同步机制,来确保在多线程环境下的线程安全性。
利用信号量机制
在C++20中,引入了计数信号量,我们可以通过使用信号量来保证线程安全:
#pragma once
#include <iostream>
#include <thread>
#include <semaphore> // 包含 semaphore 头文件
class OneInstance
{
public:
static OneInstance* getInstance()
{
//initSemaphore_.acquire();
if (instance_ == nullptr)
{
return createInstance();
}
return instance_;
}
private:
static OneInstance* createInstance()
{
// 创建实例
instance_ = new OneInstance();
//std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
// 释放信号量,表示实例已初始化完成
//initSemaphore_.release();
return instance_;
}
static OneInstance* instance_;
static std::once_flag flag_;
//static std::counting_semaphore<1> initSemaphore_; // 初始化为1,表示有一个可用信号
OneInstance()
{
std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
}
~OneInstance()
{
delete OneInstance::instance_;
OneInstance::instance_ = nullptr;
}
// 删除复制构造和赋值运算符,确保单例不可复制
OneInstance(const OneInstance&) = delete;
OneInstance& operator=(const OneInstance&) = delete;
};
// 静态成员初始化
OneInstance* OneInstance::instance_ = nullptr;
std::once_flag OneInstance::flag_; //保证只实现一次
//std::counting_semaphore<1> OneInstance::initSemaphore_{1}; // 初始信号量化为1
这时候没有引入信号量,两个线程会创建两个实例:
#define _CRT_SECURE_NO_WARNINGS 1
#include"head.h"
void task(int id)
{
OneInstance* instance = OneInstance::getInstance();
std::cout << "Thread " << id << ": Got instance with address " << instance << std::endl;
}
int main()
{
std::thread t1(task, 1);
std::thread t2(task, 2);
t1.join();
t2.join();
return 0;
}
我们把信号量放开:
#pragma once
#include <iostream>
#include <thread>
#include <semaphore> // 包含 semaphore 头文件
class OneInstance
{
public:
static OneInstance* getInstance()
{
initSemaphore_.acquire();
if (instance_ == nullptr)
{
return createInstance();
}
return instance_;
}
private:
static OneInstance* createInstance()
{
// 创建实例
instance_ = new OneInstance();
std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
// 释放信号量,表示实例已初始化完成
initSemaphore_.release();
return instance_;
}
static OneInstance* instance_;
static std::once_flag flag_;
static std::counting_semaphore<1> initSemaphore_; // 初始化为1,表示有一个可用信号
OneInstance()
{
std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
}
~OneInstance()
{
delete OneInstance::instance_;
OneInstance::instance_ = nullptr;
}
// 删除复制构造和赋值运算符,确保单例不可复制
OneInstance(const OneInstance&) = delete;
OneInstance& operator=(const OneInstance&) = delete;
};
// 静态成员初始化
OneInstance* OneInstance::instance_ = nullptr;
std::once_flag OneInstance::flag_; //保证只实现一次
std::counting_semaphore<1> OneInstance::initSemaphore_{1}; // 初始信号量化为1
两个线程得到的地址是一样的,说明我们只得到了一个实例。
上锁
这样子的方式稍微有点复杂,我们可以使用更简单的方式来完成线程的互斥:
#pragma once
#include <iostream>
#include <thread>
#include <mutex> // 包含 mutex 头文件
class OneInstance {
public:
static OneInstance* getInstance() {
std::call_once(flag_, &OneInstance::createInstance);
return instance_;
}
private:
static void createInstance() {
std::lock_guard<std::mutex> lock(mutex_); // 使用互斥量保护初始化过程
if (instance_ == nullptr) {
instance_ = new OneInstance();
std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
}
}
static OneInstance* instance_;
static std::once_flag flag_;
static std::mutex mutex_; // 互斥量用于保护初始化过程
OneInstance() {
std::cout << "Thread " << std::this_thread::get_id() << ": Inside constructor." << std::endl;
}
~OneInstance() {
delete instance_;
instance_ = nullptr;
}
// 删除复制构造和赋值运算符,确保单例不可复制
OneInstance(const OneInstance&) = delete;
OneInstance& operator=(const OneInstance&) = delete;
};
// 静态成员初始化
OneInstance* OneInstance::instance_ = nullptr;
std::once_flag OneInstance::flag_;
std::mutex OneInstance::mutex_; // 互斥量初始化
一个实例
我们利用C++11的特性(C++11及更高版本中,静态局部变量会在第一次调用其所在作用域时进行初始化,并且保证初始化过程是线程安全的。)写了一个单列模式(懒汉式)的日志记录器:
// 导入所需头文件,包括C++标准库中的输入输出流以及MySQL Connector/C++库的相关头文件
#pragma once
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <mysql_driver.h>
#include <mysql_connection.h>
#include <cppconn/statement.h>
#include <cppconn/resultset.h>
#include <cppconn/prepared_statement.h>
// 定义一个日志类,用于将日志信息输出到控制台、文件和MySQL数据库中
class Logger {
private:
// 数据库连接参数
std::string dbName = "my_log"; // 数据库名
std::string dbUser = "root"; // 用户名
std::string dbPassword = "xxxxxx"; // 密码
std::string dbHost = "xxxxxx"; // 主机地址
std::string dbPort = "3306"; // 端口号
// MySQL Connector/C++对象指针
sql::mysql::MySQL_Driver* driver;
sql::Connection* con; // 数据库连接对象
// 构造函数,用于初始化数据库连接
Logger()
{
try
{
// 获取MySQL驱动实例
driver = sql::mysql::get_mysql_driver_instance();
// 创建SQL字符串,包含数据库连接URL
sql::SQLString port(dbPort.c_str());
std::stringstream url;
url << "tcp://" << dbHost << ":" << dbPort << "/" << dbName;
// 使用用户名、密码和URL建立数据库连接
con = driver->connect(url.str().c_str(), dbUser.c_str(), dbPassword.c_str());
// 在这里可以添加其他数据库初始化代码(如创建表等操作)
}
catch (sql::SQLException& e)
{
// 捕获并处理SQL异常
std::cerr << "# ERR: SQLException occurred in \"Logger\" constructor: ";
std::cerr << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
// 在构造函数中可以选择记录日志、抛出异常或执行其他错误处理逻辑
// 如果不希望抛出异常,可以记录错误并将con设为nullptr或采取其他安全措施
}
}
// 析构函数,关闭并释放数据库连接资源
~Logger() {
delete con;
}
public:
// 获取单例实例的静态方法,内部保证线程安全
static Logger* getInstance()
{
static Logger instance; // 使用C++11的静态局部变量特性实现线程安全的懒汉式单例
return &instance;
}
// 将日志消息输出到控制台
void logToConsole(const std::string& message) {
std::cout << "Console Logger: " << message << std::endl;
}
// 将日志消息写入到文件,文件名格式为"log_年-月-日.txt"
void logToFile(const std::string& message) {
std::time_t now = std::time(nullptr); // 获取当前时间戳
std::tm* ltm = std::localtime(&now); // 转换为本地时间
std::ostringstream ss;
ss << std::put_time(ltm, "%Y-%m-%d"); // 格式化日期
//桌面路径
std::string desktop = "C:\\Users\\luoshui\\Desktop\\log\\";
std::string filename = desktop + "log_" + ss.str() + ".txt";
std::ofstream file(filename, std::ios_base::app); // 打开或创建文件,并定位到末尾
if (file.is_open())
{
file << "File Logger: " << message << std::endl;
file.close(); // 关闭文件
}
else
{
std::cerr << "Unable to open file for logging!" << std::endl;
}
}
// 将日志消息插入到MySQL数据库的logs表中
void logToDatabase(const std::string& message) {
try
{
// 准备SQL语句(假设logs表有一个message字段)
sql::PreparedStatement* pstmt = con->prepareStatement("INSERT INTO logs (message) VALUES (?)");
// 绑定参数
pstmt->setString(1, message);
// 执行更新操作
pstmt->executeUpdate();
// 关闭或删除预编译语句(根据MySQL Connector/C++的版本可能需要使用delete pstmt)
pstmt->close();
}
catch (sql::SQLException& e)
{
// 输出SQL异常信息
std::cerr << "# ERR: SQLException occurred in " << __FILE__;
std::cerr << " (" << __FUNCTION__ << ") on line " << __LINE__ << std::endl;
std::cerr << "# ERR: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
}
}
// 同时将日志消息输出到控制台、写入文件并插入数据库
void log(const std::string& message)
{
logToConsole(message);
logToFile(message);
logToDatabase(message);
}
};
饿汉式:
// 导入所需头文件,包括C++标准库中的输入输出流以及MySQL Connector/C++库的相关头文件
#pragma once
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <mysql_driver.h>
#include <mysql_connection.h>
#include <cppconn/statement.h>
#include <cppconn/resultset.h>
#include <cppconn/prepared_statement.h>
// 定义一个日志类,用于将日志信息输出到控制台、文件和MySQL数据库中
class Logger {
private:
// 数据库连接参数
std::string dbName = "my_log"; // 数据库名
std::string dbUser = "root"; // 用户名
std::string dbPassword = "xiangzihao137"; // 密码
std::string dbHost = "laptop-19ccnqk8"; // 主机地址
std::string dbPort = "3306"; // 端口号
// MySQL Connector/C++对象指针
sql::mysql::MySQL_Driver* driver;
sql::Connection* con; // 数据库连接对象
static Logger* instance_; //饿汉式
// 构造函数,用于初始化数据库连接
Logger()
{
try
{
// 获取MySQL驱动实例
driver = sql::mysql::get_mysql_driver_instance();
// 创建SQL字符串,包含数据库连接URL
sql::SQLString port(dbPort.c_str());
std::stringstream url;
url << "tcp://" << dbHost << ":" << dbPort << "/" << dbName;
// 使用用户名、密码和URL建立数据库连接
con = driver->connect(url.str().c_str(), dbUser.c_str(), dbPassword.c_str());
// 在这里可以添加其他数据库初始化代码(如创建表等操作)
}
catch (sql::SQLException& e)
{
// 捕获并处理SQL异常
std::cerr << "# ERR: SQLException occurred in \"Logger\" constructor: ";
std::cerr << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
// 在构造函数中可以选择记录日志、抛出异常或执行其他错误处理逻辑
// 如果不希望抛出异常,可以记录错误并将con设为nullptr或采取其他安全措施
}
}
// 析构函数,关闭并释放数据库连接资源
~Logger() {
delete con;
}
public:
// 获取单例实例的静态方法,内部保证线程安全
static Logger* getInstance()
{
return instance_;
}
// 将日志消息输出到控制台
void logToConsole(const std::string& message) {
std::cout << "Console Logger: " << message << std::endl;
}
// 将日志消息写入到文件,文件名格式为"log_年-月-日.txt"
void logToFile(const std::string& message) {
std::time_t now = std::time(nullptr); // 获取当前时间戳
std::tm* ltm = std::localtime(&now); // 转换为本地时间
std::ostringstream ss;
ss << std::put_time(ltm, "%Y-%m-%d"); // 格式化日期
//桌面路径
std::string desktop = "C:\\Users\\luoshui\\Desktop\\log\\";
std::string filename = desktop + "log_" + ss.str() + ".txt";
std::ofstream file(filename, std::ios_base::app); // 打开或创建文件,并定位到末尾
if (file.is_open())
{
file << "File Logger: " << message << std::endl;
file.close(); // 关闭文件
}
else
{
std::cerr << "Unable to open file for logging!" << std::endl;
}
}
// 将日志消息插入到MySQL数据库的logs表中
void logToDatabase(const std::string& message) {
try
{
// 准备SQL语句(假设logs表有一个message字段)
sql::PreparedStatement* pstmt = con->prepareStatement("INSERT INTO logs (message) VALUES (?)");
// 绑定参数
pstmt->setString(1, message);
// 执行更新操作
pstmt->executeUpdate();
// 关闭或删除预编译语句(根据MySQL Connector/C++的版本可能需要使用delete pstmt)
pstmt->close();
}
catch (sql::SQLException& e)
{
// 输出SQL异常信息
std::cerr << "# ERR: SQLException occurred in " << __FILE__;
std::cerr << " (" << __FUNCTION__ << ") on line " << __LINE__ << std::endl;
std::cerr << "# ERR: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
}
}
// 同时将日志消息输出到控制台、写入文件并插入数据库
void log(const std::string& message)
{
logToConsole(message);
logToFile(message);
logToDatabase(message);
}
};
// 在类外部定义静态成员变量,并初始化为饿汉式单例
Logger* Logger::instance_ = new Logger();