单例模式只涉及到一个单一的类,该类让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
单例模式包含如下角色:
单例模式优点:
单例模式缺点:
单例设计模式分为两种:
饿汉模式的优点是线程安全,但它的一个很明显缺点是单例创建后不一定会立即被使用,会造成一定的内存浪费。
基于静态成员变量的饿汉式单例:
#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 %
在 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 %
首先,对于静态局部变量来说,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 %
Java 并不支持静态局部变量,虽然可以借助于 synchronized
对获取单例类的方法加锁,但这样效率比较低,因此实际开发中可以基于双重检查锁实现懒汉式单例类。
在创建对象时,通常会使用 new
关键字,但需要注意这个操作并不是原子的,它实际上包括三个关键步骤:
如果编译器在优化过程中对这三个步骤进行了重排序,使执行顺序变为 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
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