单例模式作为一种设计模式,是每个程序员的必修模式。作为以前的八股代表——更多的是在面试的时候询问懒汉模式 与饿汉模式 的区别。但当前随着整体互联网大环境不好,手撕单例模式 成为了一个常态的面试问题,可以更加灵活的考察。
什么是单例模式? 单例模式是创建型设计模式,为了确保类在程序运行过程中只有一个实例,并只提供一个全局访问点,如:GetInstance()
。
如果在业务场景中,实例被共享使用,但全局只需一份的情况下,可以考虑单例模式。
懒汉模式
实例延迟加载 :被使用时才进行初始化。
线程不安全:过去的懒汉写法,在进行判空时,如果发生中断的情况下, 可能会导致创建多个实例。
实现方式:简单
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 class LazySingleton {private : static LazySingleton* instance; LazySingleton () { std::cout << "LazySingleton instance created." << std::endl; } public : static LazySingleton* getInstance () { if (instance == nullptr ) { instance = new LazySingleton (); } return instance; } void showMessage () { std::cout << "Hello from LazySingleton! instance = " << &instance << std::endl; } }; LazySingleton* LazySingleton::instance = nullptr ;
饿汉模式
实例预先创建:在类加载的时候就创建实例,不管是否使用,实例都会被创建
线程安全:类在加载时就被创建,因此不存在多线程情况下竞争,是线程安全的。
实现方式:简单,但是长时间不使用浪费资源 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class EagerSingleton {private : static EagerSingleton* instance; EagerSingleton () { std::cout << "EagerSingleton instance created." << std::endl; } public : static EagerSingleton* getInstance () { return instance; } void showMessage () { std::cout << "Hello from EagerSingleton! instance = " << &instance << std::endl; } }; EagerSingleton* EagerSingleton::instance = new EagerSingleton ();
简单测试一下生成结果
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 32 33 34 35 36 37 38 int main () { LazySingleton* singleton1 = LazySingleton::getInstance (); singleton1->showMessage (); LazySingleton* singleton2 = LazySingleton::getInstance (); singleton2->showMessage (); if (singleton1 == singleton2) std::cout << "1,2两个实例是同一个对象" << std::endl; EagerSingleton* singleton3 = EagerSingleton::getInstance (); singleton3->showMessage (); EagerSingleton* singleton4 = EagerSingleton::getInstance (); singleton4->showMessage (); if (singleton3 == singleton4) std::cout << "3,4两个实例是同一个对象" << std::endl; return 0 ; } EagerSingleton instance created. LazySingleton instance created. Hello from LazySingleton! instance = 0024E3 D0 Hello from LazySingleton! instance = 0024E3 D0 1 ,2 两个实例是同一个对象Hello from EagerSingleton! instance = 0024E3 D4 Hello from EagerSingleton! instance = 0024E3 D4 3 ,4 两个实例是同一个对象
单例模式的一致性 如果是在手撕过程中写出这两个,面试官能看出你是理解单例模式,但不够深入。上述两个只考虑了创建过程中的一致性。但是类的默认成员还有拷贝构造、赋值运算、移动构造等,这些也应该被禁止。并添加锁考虑多线程的风险。所以单例模式的通用写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Singleton { Singleton () = default ; ~Singleton () = default ; Singleton (const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; Singleton (Singleton&&) = delete ; Singleton& operator =(Singleton&&) = delete ; static Singleton* pInstance; static std::mutex lock; public : static Singleton* get () { std::lock_guard<std::mutex> lc (lock) ; if (pInstance == nullptr ) { pInstance = new Singleton; } return pInstance; } }; Singleton* Singleton::pInstance = nullptr ; std::mutex Singleton::lock;
上述单例模式可以正常使用,但是为了更好的解决线程不安全这一情况——双重检查锁模式(Double-Checked Locking Pattern,DCLP )成为了更好的方法。
有兴趣的可以查看这篇文章——DCLP的解释
上述代码被改写为
1 2 3 4 5 6 7 8 9 static Singleton* get () { if (pInstance == nullptr ) { std::lock_guard<std::mutex> lc (lock) ; if (pInstance == nullptr ) { pInstance = new Singleton; } } return pInstance; }
虽然安全性已经大大提升,但是这里还是有一个隐形的缺陷,异常情况下编译器的调用顺序。
一个在stackoverflow的调用问题
调用顺序的错误,导致DCLP机制也无法真正的做到完全安全 。因为new实例的过程是不确定的。所以现代C++11有了一个新的单例模式写法。
现代单例模式 Magic statics , more formally known asfunction-local static initialization , is when astatic variable is declared within a function, and then passed to the caller .
Since C++11, the initialization of magic statics is guaranteed to be thread safe . Internally, the first thread that calls GetString() will initialize the pointer, blocking until initialization is complete. All other threads will then use the initialized value. Before C++11, calling GetString() from multiple threads is classified as undefined behaviour (UB).
这里我直接说明一下,从C++11开始,static变量进行实例化过程时会保证线程的安全。第一个线程将初始化指针,阻塞直到初始化完成。然后,所有其他线程都将使用初始化后的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename T>class tSingleton {public : static T& getInstance () { static T instance; return instance; } tSingleton (const tSingleton&) = delete ; tSingleton& operator =(const tSingleton&) = delete ; protected : tSingleton () {} virtual ~tSingleton () {} }; class MySingleton : public tSingleton<MySingleton> {public : void doSomething () { std::cout << "Doing something in MySingleton" << std::endl; } };
个人常用的现代写法 我个人比较喜欢使用C++11的新特性,借助std::call_once
和std::once_flag
。这俩是C++11新引入的新特性,用于支持一次性初始化和线程安全的调用。它们提供了一种简单而有效的方法来确保在多线程环境下某个函数只被调用一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <mutex> class Singleton {private : static std::once_flag oc; static Singleton* instance; Singleton () {} Singleton (const Singleton&) = delete ; Singleton& operator =(const Singleton&) = delete ; public : static Singleton* getInstance () { std::call_once (oc, []{ instance = new Singleton (); }); return instance; } }; std::once_flag Singleton::oc; Singleton* Singleton::instance = nullptr ;