1.单例模式
单例模式是一种常用的软件设计模式,它的核心特征是确保一个类只有一个实例,并提供一个全局访问点。实现单例模式的关键在于控制对象的创建过程,确保不会创建多个实例。
以下是单例模式的主要特征和实现原理:
- 单一实例:单例模式要求类只能创建一个对象实例。
- 自我管理:类自身负责创建和管理这个唯一的实例。
- 全局访问:提供一个静态方法供外部获取这个唯一实例。
- 防止继承:为了防止通过继承的方式创建多个实例,通常会将构造函数设为私有。
- 防止反射攻击:在Java中,需要防止通过反射机制破坏单例的唯一性。
- 防止序列化破坏:在Java中,需要防止通过序列化和反序列化操作破坏单例的唯一性。
2.原理
实现单例模式的原理有以下几种方式:
- 饿汉式:在类加载时就创建实例,这种方式简单但可能会造成资源浪费。
- 懒汉式:在第一次使用时才创建实例,可以根据需要进行延迟加载,但需要考虑线程安全问题。
- 双重检查锁定:结合了懒汉式和同步机制,通过两次检查来确保只创建一个实例,同时减少了同步带来的性能开销。
- 静态内部类:利用Java的类加载机制保证实例的唯一性,只有在使用到内部类时才会加载,实现了懒加载。
- 枚举类:通过枚举类型来实现单例,这种方式更简洁,自动支持序列化机制,并绝对防止多次实例化。
单例模式通过以上特征和原理确保了一个类只有一个实例,并且提供了一个全局的访问点。这种模式适用于那些系统中只需要一个实例对象的场景,例如配置管理器、连接池等。
3、实现示例
cpp
#include <iostream>
// 单例类
class Singleton {
public:
// 获取单例对象的全局访问点
static Singleton& getInstance() {
static Singleton instance; // 局部静态变量,只会被初始化一次
return instance;
}
// 删除复制构造函数和赋值操作符,防止复制单例对象
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例成员函数
void doSomething() {
std::cout << "Singleton is doing something." << std::endl;
}
private:
// 将构造函数和析构函数设为私有,防止外部创建和删除实例
Singleton() {
std::cout << "Singleton created." << std::endl;
}
~Singleton() {
std::cout << "Singleton destroyed." << std::endl;
}
};
int main() {
// 获取单例对象的引用
Singleton& singleton = Singleton::getInstance();
singleton.doSomething();
system("pause");
return 0;
}
Singleton
类是单例类,它包含一个私有的构造函数和一个私有的析构函数,以防止外部创建和删除实例。getInstance
是一个静态成员函数,用于获取单例对象的全局访问点。在这个函数中,我们使用了一个局部静态变量instance
来存储单例对象。由于局部静态变量只会被初始化一次,因此我们可以确保只有一个单例对象被创建。- 我们删除了复制构造函数和赋值操作符,以防止复制单例对象。
- 在
main
函数中,我们通过调用Singleton::getInstance()
来获取单例对象的引用,并调用其成员函数doSomething。
4.双重检查锁定
要实现线程安全的单例模式,可以使用双重检查锁定(Double-Checked Locking)机制;
在C++中,双重检查锁定(Double Checked Locking)机制是一种用于实现单例模式的同步策略。它旨在减少同步带来的性能开销,同时确保线程安全。
具体来说,双重检查锁定机制在C++中通常用于实现单例模式,它包括以下几个关键步骤:
- 第一次检查:在同步块外部,首先检查实例是否已经被创建,如果已经存在,则直接返回该实例,避免了不必要的同步操作。
- 同步:如果实例尚未创建,进入同步块,这样保证了在同一时间只有一个线程能够执行实例的创建过程。
- 第二次检查:在同步块内部再次检查实例是否已经被创建,这是为了防止多个线程同时通过第一次检查后,都在等待锁时,系统可能会分配多个对象实例的情况。
- 创建实例:如果实例尚未创建,则创建实例并返回。
- 使用volatile关键字 :在某些情况下,为了防止编译器优化代码导致的双重检查锁定失效,需要将实例引用声明为
volatile
,这样可以确保实例的修改对所有线程立即可见。 - 避免指令重排:在创建实例时,为了避免JVM的指令重排问题,通常会将实例的引用赋值和实例的初始化分开写,以确保在引用赋值之前实例已经完全初始化。
需要注意的是,在C++中,双重检查锁定机制可以正确工作,但在早期版本的C++中可能会存在问题。此外,由于C++编译器的不同,双重检查锁定机制在不同编译器上的行为也可能有所不同。
总之,双重检查锁定机制是一种在多线程环境下实现单例模式的有效方法,它可以在保证线程安全的同时,提高程序的运行效率。
cpp
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取单例对象的全局访问点
static Singleton& getInstance() {
if (instance == nullptr) { // 第一次检查
std::unique_lock<std::mutex> lock(mutex); // 加锁
if (instance == nullptr) { // 第二次检查
instance = new Singleton();
}
}
return *instance;
}
// 删除复制构造函数和赋值操作符,防止复制单例对象
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例成员函数
void doSomething() {
std::cout << "Singleton is doing something." << std::endl;
}
private:
// 将构造函数和析构函数设为私有,防止外部创建和删除实例
Singleton() {
std::cout << "Singleton created." << std::endl;
}
~Singleton() {
std::cout << "Singleton destroyed." << std::endl;
}
static Singleton* instance; // 单例对象指针
static std::mutex mutex; // 互斥锁
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
int main() {
// 获取单例对象的引用
Singleton& singleton = Singleton::getInstance();
singleton.doSomething();
system("pause");
return 0;
}
在这个实现中,我们使用了双重检查锁定机制来确保在多线程环境下只创建一个单例对象。首先检查instance
是否为nullptr
,如果是,则加锁并再次检查。这样可以确保只有一个线程能够创建单例对象,从而保证线程安全。
5.懒汉式与饿汉式
懒汉式
懒汉式在第一次调用getInstance()方法时才创建实例。这种方式的优点在于,只有在真正需要实例时才会创建,节省了内存。然而,懒汉式在实现上较为复杂,需要处理多线程环境下的竞争条件,否则可能会出现多个实例的情况。
线程风险
单例模式中的懒汉式之所以会出现线程安全问题,是因为懒汉式实现的核心在于延迟实例化单例对象,即只有在真正需要时才创建实例。这种实现方式通常包括一个检查实例是否存在的条件判断,以及在不存在时创建实例的逻辑。由于这个判断和创建实例的过程可能发生在多个线程同时访问的情况下,因此存在潜在的竞态条件(race condition),从而引发线程安全问题
未加锁的懒汉式实现
cpp
class Singleton {
private:
static Singleton* instance;
// 私有构造函数,防止外部直接创建实例
Singleton() {}
public:
// 获取单例实例的静态成员函数
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
加锁的懒汉式
为了解决多线程环境下的问题,可以使用互斥锁(如std::mutex)来保护创建实例的代码块,或者使用C++11提供的std::call_once和std::once_flag来实现线程安全的单例。然而,这样的实现会使代码更为复杂,并可能引入性能开销。
cpp
#include <iostream>
#include <mutex>
class Singleton {
private:
static Singleton* instance; // 静态成员变量,在类外初始化为nullptr
static std::mutex mtx; // 静态互斥锁,用于保护instance的创建
Singleton() {} // 私有构造函数,防止外部直接创建实例
Singleton(const Singleton&) = delete; // 删除拷贝构造函数
Singleton& operator=(const Singleton&) = delete; // 删除赋值运算符
public:
static Singleton* getInstance() { // 获取单例实例的静态成员函数
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) { // 检查instance是否为nullptr
instance = new Singleton(); // 创建单例实例
}
return instance;
}
void doSomething(){
std::cout << "do something" << std::endl;
}
};
// 在类外初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx; // 初始化静态互斥锁
int main() {
Singleton* s1 = Singleton::getInstance(); // 获取单例实例
Singleton* s2 = Singleton::getInstance(); // 获取单例实例
std::cout << s1 << std::endl; // 输出单例实例的地址
std::cout << s2 << std::endl; // 输出单例实例的地址
Singleton::getInstance()->doSomething();
// 通常不建议使用system("pause"),因为它不是跨平台的。可以使用其他方法等待用户输入。
// system("pause");
std::cin.get();
return 0;
}
0x1011530
0x1011530
do something
上面不使用双重检查锁定(double-checked locking)中的外层 if (instance == nullptr)
检查。由于使用了 std::lock_guard
,它确保了加锁和解锁的原子性,所以内层的 if (instance == nullptr)
检查就足够了。双重检查锁定通常在没有内存屏障或锁的情况下使用,而 std::lock_guard
已经为我们处理了这些问题。
饿汉式
饿汉式优点是简单、安全,无需担心多线程环境下的竞争条件。然而,它的缺点是在程序启动时就占用了内存,即使这个实例在程序运行的初期并不需要使用
饿汉式是在程序启动时就创建实例的方式。它通过在类内部定义一个静态成员变量来实现,该变量在类加载时就会被初始化。具体实现如下
cpp
#include <iostream>
class Singleton {
private:
static Singleton instance; // 静态成员变量,在类外初始化
Singleton() {} // 私有构造函数,防止外部直接创建实例
public:
static Singleton& getInstance() { // 获取单例实例的静态成员函数
return instance;
}
void doSomething(){
std::cout << "do something" << std::endl;
}
};
// 在类外初始化静态成员变量
Singleton Singleton::instance;
int main() {
Singleton& s1 = Singleton::getInstance(); // 获取单例实例
Singleton& s2 = Singleton::getInstance(); // 获取单例实例
Singleton::getInstance().doSomething();
std::cout << &s1 << std::endl; // 输出单例实例的地址
std::cout << &s2 << std::endl; // 输出单例实例的地址
system("pause");
return 0;
}
do something
0x407030
0x407030
请按任意键继续. . .
定义了一个名为Singleton
的类,并在类内部定义了一个静态成员变量instance
。由于静态成员变量在类加载时就会被初始化,因此我们可以在类外对instance进行初始化。同时,我们将类的构造函数设为私有,以防止外部直接创建实例。最后,我们提供了一个静态成员函数getInstance()来获取单例实例。在main()
函数中,我们分别获取了两个单例实例,并输出了它们的地址,可以看到这两个地址是相同的,说明我们成功地实现了单例模式。
5.静态成员函数(上面均使用)
-
与类的实例的关联 :普通成员函数必须通过类的对象来调用,它们隐式地有一个指向类对象的指针(
this
指针)。而静态成员函数不属于任何对象实例,因此它们没有this指针。它们可以直接通过类名来调用,无需创建类的实例。 -
访问类的非静态成员 :由于静态成员函数没有
this
指针,因此它们不能访问类的非静态成员(包括非静态数据成员和非静态成员函数)。静态成员函数只能访问静态成员。 -
内存存储 :静态成员函数存储在程序的静态存储区,而不是对象的内存中。这意味着即使类的所有对象都被销毁,静态成员函数仍然存在。
-
用途:静态成员函数常用于执行与类本身相关但不依赖于任何特定对象实例的操作。例如,它们可能用于处理全局状态或执行一些工具性任务。
-
初始化:静态成员变量需要在类外部进行初始化,而静态成员函数则无需初始化,只需在类定义中声明并在类外部定义即可。