“对象性能”模式
面向对象很好的解决了“抽象”的问题,但是不可避免付出一定代价,如虚函数。通常情况,面向对象的成本可忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
- 单件模式
- 享元模式
单例模式
动机
- 在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性以及效率。
- 如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
- 以上要求应该是类设计者的责任,而非使用的责任。
如何实现:
1 class Singleton{ 2 private: 3 Singleton(); 4 Singleton(const Singleton& other); 5 public: 6 static Singleton* getInstance(); 7 static Singleton* m_instance; 8 }; 9 10 Singleton* Singleton::m_instance=nullptr; 11 12 //线程非安全版本 13 Singleton* Singleton::getInstance() { 14 if (m_instance == nullptr) { 15 m_instance = new Singleton(); 16 } 17 return m_instance; 18 }
首先显示声明构造函数与拷贝构造函数为私有的,避免外界调用。
其中静态方法getInstance(),在第一次调用时,条件为真,创建对象,后面再调用时,判断条件不成立,就一直只有一个对象。该方法在单线程环境下是安全的。但是在多线程条件下,就不安全。例如在ThreadA执行完14行还未执行15行new,此时ThreadB分配到时间片,执行14行也会进入到执行15行,所以多线程环境下对象可能会被创建多次。
那么如何解决?有以下思路:
加锁
1 //线程安全版本,但锁的代价过高 2 Singleton* Singleton::getInstance() { 3 Lock lock; 4 if (m_instance == nullptr) { 5 m_instance = new Singleton(); 6 } 7 return m_instance; 8 }
虽然能保证线程安全,但是锁的代价太高。分析如下:
假设线程A已经进到行4,此时线程B分到时间片,想要调用,到第3行就会不成立。如果对象已经创建,每次执行if判断都不会进入new,所以整个调用都是在判断与返回,即是在读变量m_instance,而对于读操作,是不需要加锁的。所有此时的锁会造成浪费(比如等待,锁自己也是种资源),尤其是在高并发的场景下。于是有了双检查锁:
1 //双检查锁,锁前检查锁后检查 2 Singleton* Singleton::getInstance() { 3 if(m_instance == nullptr){ 4 Lock lock; 5 if(m_instance == nullptr) { 6 m_instance = new Singleton(); 7 } 8 } 9 return m_instance; 10 }
锁前检查是为了让线程判断到对象已创建时,不用访问锁,并直接返回,这样就减少开销。在锁后检查是为了当两线程都进入第一个 if(m_instance == nullptr) 后,防止多次创建对象。
但是,双检查锁会由于内存读写reorder而失效
通常情况,代码编译时会生成指令序列,且会认为会按照指令序列执行。代码是以指令形式来抢占CPU的时间片的。以第6行为例:m_instance = new Singleton();
我们假设该语句会有以下几个部分:
Step1 :先分配内存
Step2 :调用SingleTon的构造器并对内存进行初始化
Step3 :把指向内存的指针赋值给m_instance
以上三个步骤是人认为的,但经编译器优化,CPU有可能会reorder,如下:
Step1 :先分配内存
Step2 :把指向内存的指针赋值给m_instance
Step3 :调用SingleTon的构造器并对内存进行初始化
那么就可能出现这种情况:
线程A执行到6,先分配内存,在将指向内存的指针赋给m_instance,此时轮到线程B,线程B判断到m_instance不为空,就直接返回对象,但是此时该对象还没有被构造。
所有的编译器,如果不对双检查锁的reorder漏洞处理,不能使用双检查锁,出错的概率很高。
对于Java,C#语言可以采用volatile处理,C++只有在11后才有解决方案:
1 std::atomic<Singleton*> Singleton::m_instance; 2 std::mutex Singleton::m_mutex; 3 4 Singleton* Singleton::getInstance() { 5 Singleton* tmp = m_instance.load(std::memory_order_relaxed); 6 std::atomic_thread_fence(std::memory_order_acquire); 7 if(m_instance == nullptr){ 8 std::lock_guard<std::mutex> lock(m_mutex); 9 tmp = m_instance.load(std::memory_order_relaxed); 10 if(m_instance == nullptr) { 11 m_instance = new Singleton(); 12 std::atomic_thread_fence(std::memory_order_release); 13 m_instance.store(tmp, std::memory_order_relaxed); 14 15 } 16 } 17 return tmp; 18 }
要点总结
- 单例模式中的实例构造器可以设置为protected以允许子类派生。
- 单例模式一般不要支持拷贝构造和clone接口,避免导致多个实例对象
- 如何实现多线程环境下的安全的单例模式,注意双检查锁的实现。
更多更详细的单例模式实现见:C++设计模式——单例模式