单例模式

单例模式可以说得上最简单的模式,我记得我本科毕业时找工作,很多公司问到设计模式都是谈单例模式,mvc之类的。
单例模式说简单也简单,说可研究点也很多。
怎么说呢?有时候我们想保存一个全局只需要一个变量或者对象实例的时候(比如这个对象创建很复杂但是实际使用对象实例中状态基本不会变化),会怎么做呢?
可能首先想到的是全局变量,但是从第一门 编程课开始,就有人时不时提醒自己,不要要使用全局变量,不要使用全局变量,不要使用全局变量。
那么在面向对象语言中应该怎样做到一个对象实例只有一个访问点呢?

其中关键的做法就是将构造函数与复制构造函数私有化,就可以达到这样的目的。

Singleton线程不安全版本

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   if (instance_ == nullptr) {
    instance_ = new Singleton();
    return instance_;
   }
 }
private:
 static Singleton* instance_;
};  // class Singleton

其实哈我觉得如果不是在多线程使用,其实这样写的已经够了。但是可能出现多线程下有多个线程同时通过判断if(instance_ == nullptr),从而导致单例失效。

Singleton多线程加锁

#include <mutex>

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   std::lock_guard<std::mutex> guard(single_mutex_);
   if (instance_ == nullptr) {
    instance_ = new Singleton();
    return instance_;
   }
 }
private:
 static Singleton* instance_;
 static std::mutex single_mtx_;
};  // class Singleton

这个版本呢,大家都说加锁的资源消耗大,例如其实这个过程中其实就是“写”调用构造函数的时候需要加锁,其他的时候都是“读”操作,其实是不用加锁的。而且加锁在高并发的时候很很费时间的。
但是哈,其实如果没有高并发,这样写也没问题。
上面遇到什么问题呢,“高并发的时候,不合适的加锁”,所以说加锁是一个技术活,有时候正确的地方加锁,会明显的提高性能。

双检锁

#include <mutex>

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   if (instance_ == nullptr) {
      std::lock_guard<std::mutex> guard(single_mutex_);
      if (instance_ == nullptr) {
        instance_ = new Singleton();
      }
   }
      return instance_;
 }
private:
 static Singleton* instance_;
 static std::mutex single_mtx_;
};  // class Singleton

"双检锁"顾名思义就是两次检查 if判断,一次锁 ,或者我们可以读作”检锁检“。
这个优化在哪里呢?就是如果已经创建了,直接只进行一次if判断就可以了,同时避免了多线程同时进入第一个if判断的情况。

本来觉得这个已经可以了,但是呢,实际生成中又检查出了新的问题。编译优化导致的reorder问题。
reorder是哥啥问题呢。我们看下这段代码

instance_ = new Singleton();
return instance_;

我们预想的指令调用顺序是 先申请实例的内存空间,然后调用构造函数,最后返回申请的地址。
但是编译器做了优化顺序变成了 先申请实例的内存空间,然后返回申请地址,最后调用构造函数。
这两种顺序会导致什么问题呢?
假设有非常多的线程调用GetInstance其中有一个进入第2个if,当它申请好地址后,直接先返回地址给instance_,那么其他的线程看到
instance_不为空,直接就使用instance_,而instance_并没有初始化,这不就出错了吗。

解决方案很简单,原子化和设置memory order
如何理解 C++11 的六种 memory order?
原子化很简单,atomic就是一个操作不能再细分。使用数据库的人应该明白这个基础概念。
c++11中就是 atomic
那么我们写代码吧

std::atomic<Singleton*> Single::instance_;
std::mutex Singleton::gingle_mtx_;
Singleton* Singleton::GetInstance() {
  Singleton* tmp = instance_.load(std::memory_order_relaxed);
  std::atomic_thread_fence(std::memory_order_acquire);
  if (tmp == nullptr) {
    std::lock_guard<std::mutex>  guard(single_mtx_);
    tmp = instance_.load(std::memory_order_relaxed);
    if (tmp == nullptr) {
      tmp = new Singleton();
      std::atomic_thread_fence(std::memory_order_release);
      instance_.store(tmp, std::memory_order_relaxed);
    }
  }
  return tmp;
}

使用call_once的单例

    static std::once_flag of;
    std::call_once(of, [&]() {instance_.reset(new (std::nothrow) Singleton()); });
    return instance_.get();

std::atomic_thread_fence解释

总结

  • Singleton模式中实例的构造器可以设置为protected以允许子类派生。
  • 一般不使用拷贝构造函数和原型模式。
posted @ 2020-11-23 11:15  cyssmile  阅读(102)  评论(0编辑  收藏  举报