单例模式(Singleton Pattern)是一种创建型设计模式,用于确保一个类只有一个实例,并提供全局访问点来访问该实例。单例模式通常用于需要共享资源的情况,例如数据库连接、日志记录器、缓存等
特点:
- 一个类只有一个实例:单例模式确保一个类只有一个实例对象存在
- 全局访问点:通过单例模式,可以在程序的任何地方访问该实例,而无需传递实例对象的引用
优点:
- 对于频繁使用的对象,可以减少对象创建和销毁的开销,提高性能
- 确保只有一个实例存在,避免了资源的重复占用和冲突
- 提供了对唯一实例的控制,可以实现对该实例的灵活性管理
饿汉模式
饿汉模式的单例本身就是线程安全的:
class Singleton {
public:
static Singleton* getInstance() {return m_instance;}
static void deleteInstance() {
if (m_instance) {
delete m_instance;
m_instance = nullptr;
}
}
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton &signal) = delete;
Singleton& operator=(const Singleton &signal) = delete;
private:
static Singleton *m_instance;
};
// 代码一运行就初始化创建实例 ,本身就线程安全
Singleton* Singleton::m_instance = new (std::nothrow) Singleton();
int main () {
Singleton* ps = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
或者采用 static 局部变量的形式实现单例:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
懒汉模式-naive实现
class Singleton {
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& singleton) = delete;
static Singleton* m_instance; //全局的唯一实例
public:
static Singleton* getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
static void deleteInstance() {
if (m_instance) delete m_instance;
m_instance = nullptr;
}
};
int main () {
Singleton* ps = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
Singleton* Singleton::m_instance = nullptr;
这种实现对于单线程程序是可用的,但是一旦面临多线程,就会出现线程安全的问题。下面给出线程安全的版本
懒汉模式-单检查锁实现
class Singleton {
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& singleton) = delete;
static std::mutex m_mutex;
static Singleton* m_instance; //全局的唯一实例
public:
static Singleton* getInstance() {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
static void deleteInstance() {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_instance != nullptr) delete m_instance;
m_instance = nullptr;
}
};
Singleton* Singleton::m_instance = nullptr;
std::mutex Singleton::m_mutex;
int main () {
Singleton* ps = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
这样做是线程安全的,但是每次调用 getInstance()
都需要加锁解锁,相当于将读取 m_instance
的操作进行了排队,极大地损失了效率。下面给出更高效的实现
懒汉模式-双检查锁实现
class Singleton {
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& singleton) = delete;
static std::mutex m_mutex;
static Singleton* m_instance; //全局的唯一实例
public:
static Singleton* getInstance() {
if (m_instance == nullptr) { //只有当m_instance确实为空才用加锁
std::unique_lock<std::mutex> lock(m_mutex);
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
static void deleteInstance() {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_instance != nullptr) delete m_instance;
m_instance = nullptr;
}
};
Singleton* Singleton::m_instance = nullptr;
std::mutex Singleton::m_mutex;
int main () {
Singleton* ps = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
为什么要double check?
如果我们将上面的实现的第二个检查去掉,会导致什么问题:
static Singleton* getInstance() {
if (m_instance == nullptr) { //只有当m_instance确实为空才用加锁
std::unique_lock<std::mutex> lock(m_mutex);
m_instance = new Singleton();
}
return m_instance;
}
如果有两个线程都通过了if判断,那么他们都会经历 加锁-new对象-解锁
的过程,所以new会被执行两次,违背了单例模式的规则。所以我们需要在锁前和锁后检查两次
注意双检查锁看似是线程安全的,但实际并非如此
正常的new顺序:分配内存-执行构造函数-返回内存地址
但实际在指令层面,CPU可能打乱顺序:分配内存-返回内存地址-执行构造函数
这样,当一个线程执行完“分配内存-返回内存地址”,还没来得及调用构造函数时,切换到另一个线程执行,它发现 m_instance
不是null,于是返回一个没有构造完成的“对象”,导致了错误
这是编译器对指令进行reorder优化导致的问题,各个语言几乎都存在这个问题
C++的解决方案是加入 memory_fence
class Singleton {
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& singleton) = delete;
static std::mutex m_mutex;
static std::atomic<Singleton*> m_instance;
public:
static Singleton* getInstance() {
Singleton* t = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (t == nullptr) {
std::unique_lock<std::mutex> lock(m_mutex);
t = m_instance.load(std::memory_order_relaxed);
if (t == nullptr) {
t = new Singleton();
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(t, std::memory_order_relaxed);
}
}
return t;
}
static void deleteInstance() {
std::unique_lock<std::mutex> lock(m_mutex);
if (m_instance != nullptr) delete m_instance;
m_instance = nullptr;
}
};
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
int main () {
Singleton* ps = Singleton::getInstance();
Singleton::deleteInstance();
return 0;
}
懒汉模式-call_once 实现
call_once
是 C++ 11 提供的一个库函数,用于保证代码只被执行一次:
#include<mutex>
template <class Fn, class... Args>
void call_once(once_flag& flag, Fn&& fn, Args&&...args);
class Singleton {
private:
Singleton() { std::cout << "constructor" << std::endl; }
~Singleton() { std::cout<< "destructor" << std::endl; }
Singleton(const Singleton& s) = delete;
Singleton& operator=(const Singleton& singleton) = delete;
static Singleton* m_instance; //全局的唯一实例
static once_flag oc;
public:
static Singleton* getInstance() {
call_once(oc, init);
return m_instance;
}
static void deleteInstance() {
delete m_instance;
m_instance = nullptr;
}
static void init() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
void printLog(const string& s) {
cout << s << endl;
}
};
// 类中的静态成员必须在类外初始化
Singleton* Singleton::m_instance = nullptr;
once_flag Singleton::oc;
void printError() {
auto s = Singleton::getInstance();
s->printLog("fuck");
}
int main() {
thread t1{printError};
thread t2{printError};
t1.join();
t2.join();
return 0;
}