- 对象性能模式
- 面向对象很好地解决了”抽象”的问题,但会造成一定的代价.(虚函数,继承等)
动机
- 在软件系统中,经常有这样一些特殊的类,必须保证他们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率
- 具体运用场景如:
- 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
- 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
定义
- 单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
要点
- 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
- 线程安全
- 禁止赋值和拷贝
- 用户通过接口获取实例:使用 static 类成员函数
代码实现
非线程安全的懒汉模式
懒汉式(Lazy-Initialization)
的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。
#include <iostream>
// version1:
// with problems below:
// 1. thread is not safe
// 2. memory leak
class Singleton{
private:
Singleton();
~Singleton(){}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_instance_ptr;
public:
static Singleton* get_instance(){
if(m_instance_ptr==nullptr){
m_instance_ptr = new Singleton;
}
return m_instance_ptr;
}
};
Singleton* Singleton::m_instance_ptr = nullptr;
存在问题
- 线程安全的问题
- 当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断m_instance_ptr是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance_ptr还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来
- 解决办法:加锁
- 内存泄漏
- 注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏
- 解决办法:使用共享指针
线程安全、内存安全的懒汉模式(双检查锁)
#include <memory> // shared_ptr
#include <mutex> // mutex
// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak
class Singleton{
public:
typedef std::shared_ptr<Singleton> Ptr;
static Ptr get_instance(){
// "double checked lock"
if(m_instance_ptr==nullptr){
std::lock_guard<std::mutex> lk(m_mutex);
if(m_instance_ptr == nullptr){
m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_instance_ptr;
}
private:
~Singleton(){}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr m_instance_ptr;
static std::mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
分析
- 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
- 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁(双检查锁);好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。
- 单检查锁的代价过高,单锁的话线程每次访问都会加锁,但是在创建出来单例以后线程都是读操作,并不需要加锁了.这样导致每次只能一个线程读,造成效率下降.
Singleton* Singleton::getInstance() { Lock lock; if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
- 单检查锁的代价过高,单锁的话线程每次访问都会加锁,但是在创建出来单例以后线程都是读操作,并不需要加锁了.这样导致每次只能一个线程读,造成效率下降.
问题
- 该方法会由于内存读写reorder导致不安全
- 前提:线程时间片轮转,执行的是汇编指令
- 所以对于c++代码在编译器进行优化以后,代码的执行顺序不一定是C++代码的逻辑顺序
- 可能发生的错误情况
- 在单列赋值的过程中,一般顺序是
分配内存空间->调用构造器->范围内存地址
,但可能编译器优化成分配内存空间->范围内存地址->调用构造器
,这将可能导致threadB访问的时候直接范围未初始化好的单列,导致错误.
- 在单列赋值的过程中,一般顺序是
- 解决方法: atomic
- 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束;
atomic的可跨平台的双检查线程安全的单列模式
- 要注意c/c++的标准中volatile无法解决多线程问题,只有msvc这个垃圾方言实现了防止reorder的产生.
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
基于C++11 magic static的懒汉单例模式
C++11标准中的Magic Static
- If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
- 如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
- 这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
static的声明周期(为什么可以这么写?)
- 参考:What is the lifetime of a static variable in a C++ function?
- The lifetime of function
static
variables begins the first time the program flow encounters the declaration and it ends at program termination. —- Motti- Since C++98 has no reference to multiple threads how this will be behave in a multi-threaded environment is unspecified
- In C++11 statics are initialized in a thread safe way, this is also known as Magic Statics.
- Compilers typically use a hidden flag variable to indicate if the local statics have already been initialized, and this flag is checked on every entry to the function. —-Roddy
#include <iostream>
class Singleton
{
public:
static Singleton& get_instance(){
static Singleton instance;
return instance;
}
private:
Singleton();
~Singleton();
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
};
分析
- 通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
- 不需要使用共享指针,代码简洁;
- 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。
- 这样做并不好,理由主要是无法避免用户使用delete instance导致对象被提前销毁。还是建议大家使用返回引用的方式。