C++岗位面试中,面试官常常会问道,你常用哪些设计模式。我通常会回答:单例模式、策略模式、工厂模式、模板方法模式等。 其中单例模式实现方法比较固定,面试官有时会让我手写单例模式。为了在面试中能够准确无误的写出单例模式,本文对单例模式的C++实现进行回顾与总结。
单例模式的定义 让类负责保存其自己的实例,提供一个方法(全局访问点)获取该实例,并且不能从外部创建该类的其他实例,确保该类只有一个实例。
为什么要使用单例模式,使用全局静态变量不可以吗? 全局变量无法保证只有一个实例,因此单例模式有其存在的必要。
单例模式的实现方式 单例模式通常有两种实现方式,分为“懒汉式实现”
与“饿汉式实现”
。它们的区别在于创建实例的时机。
懒汉式实现:只有需要获取实例时才创建实例。适用于不确定是否需要访问实例的情形,且实例访问非常少。
饿汉式实现:程序运行后立即创建实例。适用于需要频繁访问getInst
的情形。相当于用空间换时间。
饿汉模式的单例模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Singleton {public : static Singleton* getInst () { return &m_inst; } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static Singleton m_inst; }; Singleton Singleton::m_inst;
懒汉模式的单例模式 非线程安全实现 这是《设计模式》一书中的实现方式,是一种线程不安全的实现方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Singleton {public : static Singleton* getInst () { if (!m_inst) { m_inst = new Singleton(); } return m_inst; } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; class Garbo { public : Garbo() { } ~Garbo() { if (m_inst) { delete m_inst; } } }; private : static Singleton *m_inst; }; static Singleton *Singleton::m_inst = nullptr ;static Singleton::Garbo Singleton::m_grabo;
上述代码的Garbo类使代码十分臃肿,考虑用智能指针替代:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Singleton {public : static Singleton* getInst () { if (!m_inst) { m_inst = make_shared<Singleton>(); } return m_inst.get(); } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static shared_ptr <Singleton> m_inst; }; static shared_ptr <Singleton> Singleton::m_inst;
双重检查实现 上述实现中,没有考虑多线程访问的情形。 下面先实现一个虽然加锁保护,但是效率极低的版本。 该版本中每次调用getInst
都要访问互斥锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Singleton {public : static Singleton* getInst () { lock_guard(m_mutexInst); if (!m_inst) { m_inst = make_shared<Singleton>(); } return m_inst.get(); } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static shared_ptr <Singleton> m_inst; static mutex m_mutexInst; }; static shared_ptr <Singleton> Singleton::m_inst;static mutex Singleton::m_mutexInst;
如果先进行实例判断,再进入互斥锁,是否可行呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Singleton {public : static Singleton* getInst () { if (!m_inst) { lock_guard(m_mutexInst); m_inst = make_shared<Singleton>(); } return m_inst.get(); } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static shared_ptr <Singleton> m_inst; static mutex m_mutexInst; }; static shared_ptr <Singleton> Singleton::m_inst;static mutex Singleton::m_mutexInst;
思考一下两个线程同时进入if
语句的情况,会发现实例被创建了两次,此时引起了内存泄漏,因此该实现是错误的。
下面是双重检查的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Singleton {public : static Singleton* getInst () { if (!m_inst) { lock_guard(m_mutexInst); if (!m_inst) { m_inst = make_shared<Singleton>(); } } return m_inst.get(); } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static shared_ptr <Singleton> m_inst; static mutex m_mutexInst; }; static shared_ptr <Singleton> Singleton::m_inst;static mutex Singleton::m_mutexInst;
可以看出这个实现非常臃肿,而且它也并非完美。 根据《程序员的自我修养–链接、装载与库》一书,p29上的描述,CPU的乱序执行会导致一些问题。 主要是由于new的三个阶段也并非是原子的:
申请内存
在该内存上调用构造函数
将内存地址反回
问题在于二三步是可以颠倒的。会出现以下情况: 线程A中的m_inst = new Singleton()
已执行第三步,却未执行第二步,此时调度到线程B,判断m_inst
为非空,并返回该指针,此时m_inst
上的操作可能引起崩溃问题。
局部静态对象实现 下面是被称为Meyers' Singleton
的单例实现,使用一个局部的static变量保存单例实例。
1 2 3 4 5 6 7 8 9 10 11 class Singleton {public : Singleton& getInst () { static Singleton inst; return inst; } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; };
该实现是由著名的写出《Effective C++》系列书籍的作者Meyers提出的。所用到的特性是在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. 如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式。
这是最推荐的一种单例实现方式:
通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
不需要使用共享指针,代码简洁;
注意在使用的时候需要声明单例的引用 Single& 才能获取对象。
如果想在C++11
标准之前使用局部静态变量方式的单例模式,可以怎么做呢?恐怕必须要加锁保护静态变量的初始化了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Singleton {public : Singleton& getInst () { mutex.lock() static Singleton inst; mutex.unlock(); return inst; } private : Singleton(); Singleton(const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; private : static mutex m_mutexInst; }; static mutex Singleton::m_mutexInst;
参考文献:
C++ 单例模式总结与剖析