动机

  • 在软件系统中,经常有这样一些特殊的类,必须保证它们在系统中只存在一个实例,才能保证它们的逻辑正确性、以及良好的效率。
  • 如果绕过常规的构造器,提供一种机制来保证一个类只有一个实例。
  • 这应该是类设计者的责任,而不是使用者的责任。

定义

  • 保证一个类仅有一个实例,并提供一个该实例的全局访问点。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton{
public:
Singleton(const Singleton &) = delete;
static Singleton* getInstance();
private:
static Singleton* m_instance;
Singleton() = delete;
}

Singleton* Singleton::m_instance = nullptr;

Singleton* Singleton::getInstance() {
if(m_instance == nullptr) {
m_instance = new Singleton();
}
retrun m_instance;
}

第一个注意的问题

  • 不能让使用者获取到可以直接创建对象的构造函数,如默认构造函数、拷贝构造函数等
  • 需要将他们设置为私有或delete

第二个注意的问题

  • 上述版本中getInstance是一个线程不安全的版本,在多线程环境中使用时需要进一步改进

改进一:锁保护

1
2
3
4
5
6
7
Singleton* Singleton::getInstance() {
Lock lock;
if(m_instance == nullptr) {
m_instance = new Singleton();
}
retrun m_instance;
}
  • 这个版本通过锁对m_instance进行了保护,是一个线程安全的版本
  • 但是锁其实仅在第一次创建对象时才会起到真实的保护作用,后续锁就会影响性能
  • 根源是因为锁不紧用在了变量写操作,同时作用到了变量读操作,产生了性能消耗

改进二:双检查锁

1
2
3
4
5
6
7
8
9
Singleton* Singleton::getInstance() {
if(m_instance == nullptr) {
Lock lock;
if(m_instance == nullptr) {
m_instance = new Singleton();
}
}
retrun m_instance;
}
  • 这个版本仅在第一次创建对象时锁才会起作用,后续就不会占用资源,从而避免锁的性能消耗
  • 但是这个版本有一个隐藏很深的bug:由于内存读写reorder导致的锁失效问题
  • 内存读写reorder
    • 多线程是在指令层级抢占cpu时间片的
    • 编译器对代码指令级的优化导致实际执行顺序可能发生改变
    • 在这个例子中m_instance = new Singleton();指令可能分为三个步骤:分配内存、调用构造器、返回内存地址
    • 实际编译器优化后顺序可能为:分配内存、返回内存地址、调用构造器
    • 如果执行到第二个步骤后,另一个线程调用getInstance()就会拿到一个未执行构造的对象指针,引发bug

改进三:规避reorder问题

  • 编译器的支持:关键字volatile声明变量m_instance()即可
  • 跨平台支持(c++11)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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;
    }

总结

  • Singleton模式中的实例构造器可以设置为protected以允许子类派生。
  • Singleton模式一般不需要支持拷贝构造函数和clone接口,因为这可能导致多个对象实例,违背初衷。
  • 如何实现多线程环境下安全的Singleton?注意对双检查锁的正确实现。