单例模式只涉及到一个单一的类,该类让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
单例模式包含如下角色:
- 单例类:创建并维持一个唯一实例的类;
- 访问类:使用单例类。
单例模式优点:
- 保证一个类只有一个实例,这对于线程池和连接池等池化对象很有意义。
- 仅需要在首次请求单例对象时对其进行初始化,减少了初始化开销。
单例模式缺点:
- 单例模式同时解决了保证一个类只有一个实例和为该实例提供一个全局访问节点两个问题, 所以违反了单一职责原则。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
单例设计模式分为两种:
- 饿汉式:类加载时就会创建该单实例对象。
- 懒汉式:单实例对象不会在类加载时被创建,而是在首次使用该对象时创建。
一、饿汉式
饿汉模式的优点是线程安全,但它的一个很明显缺点是单例创建后不一定会立即被使用,会造成一定的内存浪费。
基于静态成员变量的饿汉式单例:
#include "main.h"
class Singleton {
private:
/* 让构造函数私有以避免类被实例化 */
Singleton() {
cout << "Singleton()" << endl;
}
/* 类对应的唯一的对象 */
static Singleton *m_instance;
public:
/* 实例对象的唯一访问方式 */
static Singleton *getInstance() {
return m_instance;
}
~Singleton() {
cout << "~Singleton()" << endl;
}
};
/* 静态成员变量需要类内定义类外初始化 */
Singleton *Singleton::m_instance = new Singleton();
int main() {
Singleton *singleton1 = Singleton::getInstance();
Singleton *singleton2 = Singleton::getInstance();
Singleton *singleton3 = Singleton::getInstance();
cout << singleton1 << endl;
cout << singleton2 << endl;
cout << singleton3 << endl;
delete singleton1;
return 0;
}
atreus@AtreusMBP 9:39 code % g++ main.cpp -o main -std=c++17
atreus@AtreusMBP 11:35 code % ./main
Singleton()
0x6000029d4030
0x6000029d4030
0x6000029d4030
~Singleton()
atreus@AtreusMBP 11:35 code %
二、懒汉式
2.1 基于双重检查锁(C++)
在 C++ 中通过静态成员变量实现的懒汉式单例类主要需要解决两个问题:
- 线程安全:需要在实例化单例前进行加锁,不然很有可能实例化多个单例。
- 内存安全:一方面是可能忘记
delete
,内存安全完全托管给了 OS。另一方面在一些特殊的情况下也有可能出现重复释放单例类的问题。
对于线程安全问题,可以通过加锁解决,不过这里要进行两次 m_instance == nullptr
检查,避免多个线程同时通过第一次判断,然后依次获取锁,从而重复创建单例类。而对于内存安全的问题,可以通过 RAII 机制解决。
基于双重检查锁和 RAII 的懒汉式单例:
mutex singleton_mutex;
class Singleton {
private:
/* 让构造函数私有以避免类被实例化 */
Singleton() {
cout << "Singleton()" << endl;
}
/* 类对应的唯一的对象 */
static Singleton *m_instance;
public:
/* 实例对象的唯一访问方式 */
static Singleton *getInstance() {
if (m_instance == nullptr) {
lock_guard<mutex> lock(singleton_mutex);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
~Singleton() {
cout << "~Singleton()" << endl;
}
};
/* 静态成员变量需要类内定义类外初始化 */
Singleton *Singleton::m_instance = nullptr;
class SingletonRAII {
public:
Singleton *m_singleton;
explicit SingletonRAII(Singleton *singleton) {
m_singleton = singleton;
}
~SingletonRAII() {
delete m_singleton;
}
Singleton *getSingleton() const {
return m_singleton;
}
};
void threadFunction(SingletonRAII *singleton_raii) {
Singleton *singleton = singleton_raii->getSingleton();
cout << singleton << endl;
}
int main() {
SingletonRAII singleton_raii(Singleton::getInstance());
vector<thread> threads;
threads.reserve(10);
for (int i = 0; i < 10; i++) {
threads.emplace_back(threadFunction, &singleton_raii);
}
for (std::thread &thread: threads) {
thread.join();
}
return 0;
}
atreus@AtreusMBP 12:19 code % g++ main.cpp -o main -std=c++17
atreus@AtreusMBP 12:37 code % ./main
Singleton()
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
0x60000237c030
~Singleton()
atreus@AtreusMBP 12:37 code %
2.2 基于静态局部变量(C++)
首先,对于静态局部变量来说,static
关键字并没有改变它的局部作用域,当定义它的函数或者语句块结束时,作用域也随之结束。但是当静态局部变量离开作用域后,它并没有销毁,仍然驻留在内存当中,只是暂时无法被访问,只要我们再次调用 getInstance()
方法,就能重新得到这个静态局部变量。当程序结束时,该静态局部变量的内存会被自动释放,单例类也会被自动析构,因此内存安全得以保证。
其次,对于线程安全的问题,GCC 等编译器已经支持了静态变量构造和析构函数的多线程安全。以构造函数为例,对于局部静态变量,多线程调用时,首先构造静态变量的线程先加锁,其他线程等锁,因此线程安全也得以保证。实际上,静态局部变量的多线程安全是与编译选项 -fno-threadsafe-statics
直接挂钩的,而此选项在不同编译器中都默认打开。
基于静态局部变量的懒汉式单例:
class Singleton {
private:
/* 让构造函数私有以避免类被实例化 */
Singleton() {
cout << "Singleton()" << endl;
}
public:
/* 实例对象的唯一访问方式 */
static Singleton *getInstance() {
static Singleton instance; // 静态成员函数里的静态局部变量等效于类的静态成员变量
return &instance;
}
~Singleton() {
cout << "~Singleton()" << endl;
}
};
void threadFunction(int threadID) {
Singleton *singleton = Singleton::getInstance();
std::cout << singleton << std::endl;
}
int main() {
vector<thread> threads;
threads.reserve(10);
for (int i = 0; i < 10; i++) {
threads.emplace_back(threadFunction, i);
}
for (std::thread &thread: threads) {
thread.join();
}
return 0;
}
atreus@AtreusMBP 11:58 code % g++ main.cpp -o main -std=c++17
atreus@AtreusMBP 12:17 code % ./main
Singleton()
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
0x102038000
~Singleton()
atreus@AtreusMBP 12:17 code %
2.3 基于双重检查锁(Java)
Java 并不支持静态局部变量,虽然可以借助于 synchronized
对获取单例类的方法加锁,但这样效率比较低,因此实际开发中可以基于双重检查锁实现懒汉式单例类。
在创建对象时,通常会使用 new
关键字,但需要注意这个操作并不是原子的,它实际上包括三个关键步骤:
分配内存
:为 Singleton 实例分配堆内存空间。调用构造函数
:在分配的内存上初始化对象。设置引用
:将分配的内存地址赋值给 instance 变量。
如果编译器在优化过程中对这三个步骤进行了重排序,使执行顺序变为 1 -> 3 -> 2
,可能会导致一个潜在的问题:引用变量指向已分配的内存,但对象尚未完全初始化,这可能引发线程安全问题,导致其他线程获取到一个尚未初始化的单例类。因此需要使用 volatile
关键字,它通过内存屏障保证赋值操作执行时前序构造操作均已完成,后续读取操作均未开始,防止指令重排序,从而保证线程安全性。
基于双重检查锁的懒汉式单例:
public class Singleton {
/* 让构造函数私有以避免类被实例化 */
private Singleton() {}
/* 类对应的唯一的对象 */
private volatile static Singleton instance;
/* 实例对象的唯一访问方式 */
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
class SingletonTest {
@Test
public void getInstanceTest() {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Singleton instance = Singleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
atreus.ink.Singleton@77239cbf
2.4 基于静态内部类(Java)
Java 中还可以通过静态内部类实现懒汉式单例。
与静态成员变量在类加载时初始化不同,静态内部类只有在首次访问时才会加载和初始化,也就是说它们的加载是按需延迟的,不会随着外部类的加载而自动加载,同时初始化过程线程安全没有同步开销。
基于静态内部类的懒汉式单例:
public class Singleton {
/* 让构造函数私有以避免类被实例化 */
private Singleton() {}
/* 静态内部类只有在首次访问时才会加载和初始化 */
private static class InnerClass {
private final static Singleton instance = new Singleton();
}
/* 实例对象的唯一访问方式 */
public static Singleton getInstance() {
return InnerClass.instance;
}
}
参考:
https://refactoringguru.cn/design-patterns/singleton