线程安全的单例模式

前言

单例模式 ( Singleton Pattern ) 是最简单的、也是我们很常用的一种设计模式。保证⼀个类仅有⼀个实例,并提供⼀个该实例的全局访问点。那么在多线程的环境,怎么才能更好的确保线程安全呢?

实现

1. 饿汉模式

饿汉模式使用一个静态成员变量,程序启动即完成构造,不用考虑线程安全的问题,c++ 11static 的特性:如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束

class Singleton {
public:
  static Singleton *GetInstance() {
    return instance_;
  }

  static void DestreyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
    }
  }

private:
  Singleton() = default;
  Singleton& operator=(const Singleton&) = delete;
  Singleton(const Singleton& singleton2) = delete;

private:
  static Singleton *instance_;
};

Singleton* Singleton::instance_ = new Singleton;

int main() {
  Singleton* s1 = Singleton::GetInstance();
  Singleton::DestreyInstance();
  return 0;
}
关于资源的释放

上例中我们使用了一个静态的成员函数来释放堆上的资源,需要手动调用,并不是很方便。我们可以使用智能指针 或者一个内部类来实现资源的自动释放,也更符合 Modern cpp 的风格。接下来我们演示如何使用一个内部类来优化饿汉模式。

class Singleton {
public:
  static Singleton *GetInstance() {
    return instance_;
  }

  static void DestreyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
    }
  }
private:
  class GC {
    public:
      ~GC()  {
        // 可以在这里销毁所有的资源,例如:db 连接、文件句柄等
        if (instance_ != nullptr) {
          delete instance_;
          instance_ = nullptr;
        }
      }
      static GC gc; 
  };

private:
  Singleton() = default;
  Singleton& operator=(const Singleton&) = delete;
  Singleton(const Singleton& singleton2) = delete;

private:
  static Singleton *instance_;
};

Singleton* Singleton::instance_ = new Singleton;

懒汉模式

饿汉方式不论是否需要使用该对象都将其定义出来,可能浪费了内存,或者减慢了程序的启动速度。所以使用懒汉模式进行优化,懒汉模式即延迟构造对象,在第一次使用该对象的时候才进行 new 该对象。

而懒汉模式会存在线程安全问题,最出名的解决方案就是 Double-Checked Locking Pattern (DCLP)(双检锁)。使用两次判断来解决线程安全问题并且提高效率。

class Singleton {
public:
  static Singleton& GetInstance() {
    if (instance_ == nullptr) {
      std::lock_guard<std::mutex> lock(mutex_);
      if (instance_ == nullptr) {
        instance_.reset(new Singleton);
      }
    }
    return *instance_;
  }
  ~Singleton() = default;

private:
  Singleton() = default;
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
  static std::mutex mutex_;
};

std::unique_ptr<Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;
双检锁的问题

DCLP 实际上也是存在这严重的线程安全问题的,具体参考 C++ and the Perils of Double-Checked Locking

原文中有这样一段话:

Consider again the line that initializes pInstance:

pInstance = new Singleton;

This statement causes three things to happen:

​ Step 1: Allocate memory to hold a Singleton object.

​ Step 2: Construct a Singleton object in the allocated memory.

​ Step 3: Make pInstance point to the allocated memory.

Of critical importance is the observation that compilers are not constrained to perform these steps in this order! In particular, compilers are sometimes allowed to swap steps 2 and 3. Why they might want to do that is a question we’ll address in a moment. For now, let’s focus on what happens if they do.

也就是我们在

instance_ = new Singleton;

时发生了三件事情:

  1. 申请一块内存来保存单例对象。
  2. 在申请的内存中调用构造函数。
  3. 将内存的地址赋值给 instance_

不同的编译器表现不一样,它们并不会严格按照这个顺序执行,尤其是有时候允许交换第二步和第三步! 也就是说可能会先将内存地址赋值给 instance_ 然后再调用构造函数。这样就会有严重的线程安全问题,比如:线程 A 刚好申请完内存并将该内存地址赋值给 instance_ 但是此时还没调用构造函数,又刚好此时线程 B 执行到了

if (instance_ == nullptr) 判断 instance_ 并不为空,返回了该变量,然后调用了该对象的函数,但是该对象还没进行构造。其实就是编译优化导致的一个乱序 (reorder) 的问题。

懒汉模式进一步优化

1. 使用 std::call_once

c++ 11std::call_oncestd::once_flag 配合使用使得函数可以线程安全的只调用一次。

class Singleton {
public:
  static Singleton& GetInstance() {
    static std::once_flag s_flag;
    std::call_once(s_flag, [&]() {
      instance_.reset(new Singleton);
    });

    return *instance_;
  }

  ~Singleton() = default;

  void PrintAddress() const {
    std::cout << this << std::endl;
  }

private:
  Singleton() = default;
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
};

std::unique_ptr<Singleton> Singleton::instance_;

2. 使用内存屏障

class Singleton {
 public:
  static 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> lock(mutex_);
      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);
        atexit(Destructor);
      }
    }
    return tmp;
  }
 private:
  static void Destructor() {
    Singleton* tmp = instance_.load(std::memory_order_relaxed);
    if (nullptr != tmp) {
      delete tmp;
    }
  }
  Singleton() = default;
  Singleton(const Singleton&) {}
  Singleton& operator=(const Singleton&) {}
  static std::atomic<Singleton*> instance_;
  static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_;
std::mutex Singleton::mutex_; 

最简单实用的方式

好了,说了这么多,前面一顿操作猛如虎,其实最简单实用的还是静态局部变量的方式。

class Singleton {
 public:
  ~Singleton(){}
  static Singleton& GetInstance() {
    static Singleton instance;
    return instance;
  }
 private:
  Singleton() = default;
  Singleton(const Singleton&) {}
  Singleton& operator=(const Singleton&) {}
};

优点:

  • 代码简洁;
  • 利⽤静态局部变量特性,延迟加载;
  • 利⽤静态局部变量特性,系统⾃动回收内存,⾃动调⽤析构函数;
  • 静态局部变量初始化时,没有 new 操作带来的 cpu 指令 reorder 操作;
  • c++11 静态局部变量初始化时,具备线程安全;

升级为模板

template<typename T>
class Singleton {
 public:
  static T& GetInstance() {
    static T instance;
    return instance;
  }
 protected:
  virtual ~Singleton() {}
  Singleton() {}
  Singleton(const Singleton&) {}
  Singleton& operator =(const Singleton&) {}
};
使用方式

比如我们有一个普通类 DesignPattern ,它是全局唯一的,那么我们可以

Singleton<DesignPattern>::GetInstance();

这样使用。

或者

class DesignPattern : public Singleton<DesignPattern> {
  friend class Singleton<DesignPattern>;
 private:
  DesignPattern(){}
  DesignPattern(const DesignPattern&) {}
  DesignPattern& operator=(const DesignPattern&) {}
};

直接

DesignPattern::GetInstance();
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值