单例模式介绍及单例模式的线程安全问题
单例模式的定义:
单例模式是指对象在内存中只会创建且仅被创建一次的设计模式,这种设计模式被用来解决对象被频繁调用时的反复创建给设备带来较大负担的情况。
单例模式的分类:
饿汉模式:在类加载的时候就创建对象,天然线程安全
懒汉模式:在需要使用时才创建对象,可能出现线程访问问题
饿汉模式的特点:
1.提前实例化
饿汉模式会在类被加载的时候立即创建实例,例如,通过静态成员变量的初始化,在程序启动时就完成实例的创建。
2.线程安全
由于实例被创建在类加载的时候,所以在绝大多数情况下线程都是安全的,多线程访问该实例的时候不会产生竞争条件。
3.简单性
实现非常简单且代码易于理解,因为我们通过硬编码创建了实例,所以可以舍弃非常多不必要的额外检查。
饿汉模式的代码:
#include <iostream> using namespace std; class singleton { private: static singleton instance;//静态成员,保持唯一实例 singleton(){}//构造函数私有化,禁止外部创建实例 ~singleton(){}//析构函数私有化,禁止外部销毁实例 singleton(const singleton&) = delete;//删除拷贝构造函数 singleton& operator=(const singleton&) = delete;//删除拷贝赋值运算符 public: static singleton& getInstance()//获取实例的静态成员函数 { return instance; } void show() { std::cout << "this is a singleton" << std::endl; } }; singleton singleton::instance;//静态成员初始化 int main(int argc,const char* argv[]) { singleton& single = singleton::getInstance(); single.show(); return 0; }
懒汉模式的特点:
1.延迟创建
懒汉模式会在调用单例获取方法时检查实例是否存在,只有在需要使用实例时才会进行创建。这种方式可以减少初始化的资源占用,特别是当实例的创建成本较高,且运行时不总是需要使用单例时非常有效。
2.线程安全
在多线程环境下,懒汉模式的实例创建需要额外的同步机制,以确保多个线程访问时不会创建多个实例,我们通常选择使用互斥锁来防止竞争。
3.资源节约
由于仅在第一次调用时创建实例,可以节省在程序启动时的资源,为那些并不需要单例的类节省内存和创建成本。
4.代码复杂性
相比饿汉模式,懒汉模式的实现稍微复杂一些,因为需要考虑多线程安全性和实例状态的管理。这可能会导致代码的可读性降低。
如何解决懒汉模式的线程安全问题:
懒汉模式的线程安全问题主要源于在多线程环境中多个线程同时访问获取单例实例的静态方法时,可能导致多个线程同时创建出多个实例。
我们很容易就能想到加锁来解决线程的竞争问题,我们先看一下如何用加锁来解决这个问题:
#include <iostream> #include <mutex> class singleton { private: static singleton* instance;//单例对象 static std::mutex mtx;//互斥锁 singleton() {};//私有构造函数 ~singleton() {};//私有析构函数 public: static singleton& get_instance()//获取单例对象的静态方法 { std::lock_guard<std::mutex> lock(mtx); if (instance == nullptr) { instance = new singleton(); } return *instance; } void show() { std::cout << "This is a singleton" << std::endl; } singleton(const singleton&) = delete; //禁止拷贝构造函数 singleton& operator=(const singleton&) = delete; //禁止拷贝赋值运算符 }; //初始化 singleton* singleton::instance = nullptr; std::mutex singleton::mtx; int main(int argc, char* argv[]) { singleton& singleton = singleton::get_instance(); singleton.show(); return 0; }
这个例子是一个简单的加锁来解决线程竞争的问题,通过代码能看出我们每次获取对象都要尝试去加锁,在多线程高并发场景下会极大的影响性能,因此我们可以使用另一种方法——双重检查锁定。
#include <iostream> #include <mutex> class singleton { private: static singleton* instance;//单例对象 static std::mutex mtx;//互斥锁 singleton() {};//私有构造函数 ~singleton() {};//私有析构函数 public: static singleton& get_instance() // 获取单例对象的静态方法 { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mtx); // 加锁 // 锁内再检查一次 if (instance == nullptr) { instance = new singleton(); // 创建实例 } } return *instance; // 确保返回实例 } void show() { std::cout << "This is a singleton" << std::endl; } singleton(const singleton&) = delete; //禁止拷贝构造函数 singleton& operator=(const singleton&) = delete; //禁止拷贝赋值运算符 }; //初始化 singleton* singleton::instance = nullptr; std::mutex singleton::mtx; int main(int argc, char* argv[]) { singleton& singleton = singleton::get_instance(); singleton.show(); return 0; }
但是这个方法依然存在问题——指令重排。这个问题我们可以使用c++11引入的std::call_once解决,它的函数原型如下:
#include <mutex> void std::call_once(std::once_flag& flag, Callable&& f, Args&&... args);
-
flag
: 这是一个std::once_flag
类型的对象,用来标记该操作是否已经被执行过。 -
f
: 这是一个可调用对象(比如函数指针、lambda 表达式等),在call_once
被调用时执行。 -
args
: 可选的参数,传递给f
的参数。
std::call_once
确保某个函数 f
只会被调用一次。即使在多线程环境下,从任何线程调用这个函数,都会保证 f
只会执行一次,确保线程安全。