单例模式通常被归类为创建型设计模式,因为它主要关注如何创建对象的实例,以及如何确保在整个应用程序生命周期中只有一个实例存在。
1.为什么日志模块和数据库连接池需要单例模式
使用单例模式来实现数据库连接池主要有以下几个原因:
全局唯一性:数据库连接池在应用程序中通常是一个全局资源,多个部分都需要共享同一个连接池。使用单例模式可以确保整个应用程序中只存在一个数据库连接池的实例,避免了资源浪费和连接冲突。
统一管理:单例模式可以提供一个统一的访问点来管理数据库连接池,使得连接的创建、释放和管理更加方便和可控。
避免资源竞争:在多线程环境下,如果每次请求都创建一个新的连接池实例,可能会导致资源竞争和线程安全问题。使用单例模式可以避免这种情况,确保连接池的唯一性和线程安全性。
节省资源:数据库连接池是一种重量级对象,频繁地创建和销毁连接池实例会消耗大量系统资源,而单例模式可以确保连接池的唯一性,节省了系统资源的开销。
全局唯一性: 日志模块是应用程序中的重要组成部分,通常在整个应用程序中都需要被访问和使用。使用单例模式可以确保在应用程序的生命周期内只存在一个日志模块实例,避免了资源的重复创建和浪费。
统一管理: 单例模式提供了一个统一的访问点,可以方便地管理日志记录。所有对日志模块的操作都通过单例对象进行,使得管理和维护更加简单和可控。
避免资源竞争: 在多线程环境下,如果多个线程同时尝试创建日志模块实例,可能会导致资源竞争和线程安全问题。单例模式可以确保在并发情况下只创建一个实例,避免了资源竞争和线程安全性问题。
节省资源: 日志模块通常是一种重量级对象,频繁地创建和销毁会消耗大量系统资源。使用单例模式可以确保只有一个实例存在,节省了系统资源的开销。
一致性和可靠性: 使用单例模式可以确保日志记录的一致性和可靠性。由于只有一个日志模块实例存在,所有的日志记录都会经过同一个对象,不会出现不一致或漏掉日志的情况。
2. 饿汉式单例模式:
它在类加载时就创建了实例对象,并且在整个程序的生命周期中始终存在。这种方式的单例模式在多线程环境下是线程安全的,因为实例对象在类加载时就已经创建好了,不会出现多个线程同时创建实例的情况。
cpp
// 饿汉单例 ,一定是安全的
class Singleton{
public:
static Singleton* getInstance(){ // 3. 获取类的唯一实例对象的接口方法
return &instance;
}
private:
static Singleton instance; // 2. 定义一个唯一的类的实例对象
Singleton(){ // 1. 构造函数私有化
}
Singleton(const Singleton&) =delete; //4.禁止拷贝构造 Singleton& 引用类型
Singleton& operator= (const Singleton&)=delete; // 5. 禁止赋值函数
};
Singleton Singleton::instance; // 在数据段,main函数启动前,已经初始化好了,但是影响启动时间,构造函数用不了,比如加载文件
int main(){
Singleton *p1 = Singleton::getInstance();
std::cout << p1 << std::endl;
return 0;
}
3. 懒汉模式
懒汉式单例模式是一种延迟实例化的单例模式实现方式,在第一次被调用时才会创建实例对象。与饿汉式单例模式不同,懒汉式单例模式的实例对象在需要时才被创建,因此称为"懒汉"。
cpp
class Singleton{
public:
// 是不是可重入函数? 锁+ 双重判断
static Singleton* getInstance(){ // 3. 获取类的唯一实例对象的接口方法
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
/*
开辟空间
给instance 赋值
构造对象
*/
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
private:
/*
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。
volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,
都会直接从变量地址中读取数据。如 果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,
如果这个变量由别的程序更新了的话,将出现不一致的现象。所以遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,
从而可以提供对特殊地址的稳定访问。
##########################################################################################
cpu为了加速访问,会将内存的值拷贝一份到寄存器中,加入volatile
*/
static Singleton *volatile instance; // 2. 定义一个唯一的类的实例对象
// 用于保护多线程环境下的线程安全
static std::mutex mutex;
Singleton(){ // 1. 构造函数私有化
}
Singleton(const Singleton&) =delete; //4.禁止拷贝构造 Singleton& 引用类型
Singleton& operator= (const Singleton&)=delete; // 5. 禁止赋值函数
};
Singleton*volatile Singleton::instance = nullptr;
std::mutex Singleton::mutex;
int main(){
Singleton *p1 = Singleton::getInstance();
std::cout << p1 << std::endl;
return 0;
}
cpp
class Singleton{
public:
static Singleton* getInstance(){ // 3. 获取类的唯一实例对象的接口方法
static Singleton instance; // 2. 定义一个唯一的类的实例对象
// 函数的静态局部变量的初始化,在汇编指令上已经自动添加线程互斥指令了
return &instance;
}
private:
Singleton(){
}
Singleton(const Singleton&) =delete; //4.禁止拷贝构造 Singleton& 引用类型
Singleton& operator= (const Singleton&)=delete; // 5. 禁止赋值函数
};
3. 可重入函数
可重入函数是指在多线程环境下能够安全地被多个线程同时调用的函数。具体来说,可重入函数满足以下两个条件之一或两者兼具:
- 函数不使用静态变量或全局变量(包括静态局部变量),或者使用这些变量时通过互斥锁或其他同步机制来保护变量的访问。
- 函数使用的静态变量或全局变量只读,或者在使用时通过互斥锁或其他同步机制来保护变量的访问。
在多线程环境下,如果一个函数不满足上述条件之一,那么当多个线程同时调用该函数时,就会出现数据竞争和不确定的行为,导致程序的行为变得不可预测,甚至引发崩溃。
编写可重入函数的关键是避免共享可变状态,或者在共享状态时采取适当的同步措施,以确保线程安全性。这可能涉及到使用局部变量而不是静态变量或全局变量,使用线程局部存储(Thread-Local Storage,TLS)等技术来避免共享状态,以及使用互斥锁、信号量、原子操作等同步机制来保护共享状态的访问。
cpp
#include <iostream>
#include <mutex>
// 可重入函数示例
int reentrantFunction(int value) {
// 局部变量,线程私有,不会造成数据竞争
int result = 0;
result += value * 2;
return result;
}
int main() {
// 多个线程同时调用可重入函数
int result1 = reentrantFunction(5);
int result2 = reentrantFunction(10);
// 打印结果
std::cout << "Result 1: " << result1 << std::endl;
std::cout << "Result 2: " << result2 << std::endl;
return 0;
}