😉一、基础概念
单例模式(Singleton Pattern)是一种创建型设计模式,它保证某一个类只有一个实例存在,并提供一个全局访问点。
在单例模式中,通过将类的构造函数设为私有,从而防止外部直接创建对象。同时,通过一个静态方法或者变量来获取该类的唯一实例。如果该类的实例不存在,则创建一个新的实例并返回;如果该类的实例已经存在,则直接返回该实例。
单例模式的优点是可以避免因为创建多个实例而导致的资源浪费和性能下降。同时,由于该类只有一个实例存在,因此可以更好地控制该实例的状态和行为。此外,单例模式还可以提供一个全局访问点,使得其他对象可以方便地访问该实例。
需要注意的是,单例模式虽然可以解决某些问题,但也可能会引入新的问题。例如,单例模式可能会导致代码的耦合性增加,使得代码难以测试和维护。因此,在使用单例模式时需要权衡利弊,合理使用。
🐱🐉二、单例模式实现
懒汉模式
class Singleton { public: static Singleton* getInstance() { static Singleton instance; return &instance; } void doSomething() { std::cout << "Singleton doSomething" << std::endl; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; }; int main() { Singleton* instance1 = Singleton::getInstance(); Singleton* instance2 = Singleton::getInstance(); if (instance1 == instance2) { std::cout << "instance1 and instance2 are the same instance" << std::endl; } else { std::cout << "instance1 and instance2 are different instances" << std::endl; } instance1->doSomething(); instance2->doSomething(); return 0; }
在这个示例中,Singleton
类的构造函数是私有的,因此外部无法直接创建该类的实例。getInstance()
方法是该类的静态方法,用于获取该类的唯一实例。在 getInstance()
方法中,使用了静态局部变量的方式来创建该类的唯一实例。由于静态局部变量只会在第一次调用时初始化,因此可以保证该类只有一个实例存在。
需要注意的是,在多线程环境下,需要考虑线程安全问题。可以使用加锁的方式来解决该问题。此外,在使用单例模式时,还需要避免使用全局变量或静态变量等方式来创建对象,这样会破坏单例模式的原则。
饿汉模式
class Singleton { public: static Singleton* getInstance() { return &instance; } void doSomething() { std::cout << "Singleton doSomething" << std::endl; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton instance; }; Singleton Singleton::instance; int main() { Singleton* instance1 = Singleton::getInstance(); Singleton* instance2 = Singleton::getInstance(); if (instance1 == instance2) { std::cout << "instance1 and instance2 are the same instance" << std::endl; } else { std::cout << "instance1 and instance2 are different instances" << std::endl; } instance1->doSomething(); instance2->doSomething(); return 0; }
在这个示例中,Singleton
类的唯一实例是在类定义中直接创建的,因此可以保证在程序启动时就已经创建了该实例,即所谓的“饿汉模式”。需要注意的是,由于该实例是在程序启动时就创建的,因此可能会影响程序启动的速度和内存占用。
在使用饿汉单例模式时,需要注意线程安全问题。如果该类的实例需要在多线程环境下使用,需要使用加锁的方式来保证线程安全。
🎉三、单例模式与线程安全
单例模式的懒汉模式和饿汉模式都可能会造成线程安全问题,具体原因如下:
- 懒汉模式
懒汉模式是指在第一次使用时才创建单例对象。在多线程环境下,如果多个线程同时调用 getInstance()
方法,那么可能会创建多个实例,违反了单例模式的原则。例如,当一个线程在判断实例是否存在时,还没有创建实例,此时另一个线程也进入了判断实例是否存在的代码块,也未创建实例,然后两个线程都创建了实例,导致单例模式失效。
- 饿汉模式
饿汉模式是指在程序启动时就创建单例对象。在多线程环境下,如果多个线程同时调用 getInstance()
方法,那么可能会访问到未完全初始化的实例,从而导致程序异常或者崩溃。例如,当一个线程正在初始化实例时,另一个线程也调用了 getInstance()
方法,此时可能会访问到未完全初始化的实例。
因此,在实际使用中,需要考虑单例模式的线程安全问题。可以使用加锁的方式来保证线程安全,或者采用其他线程安全的方式来实现单例模式。
🐱🚀四、懒汉模式怎么保证线程安全
如果要求在懒汉模式下保证线程安全,并且实例化之后不能再加锁,可以使用双重检查锁定(Double-Checked Locking)的方式来实现。双重检查锁定是一种常用的单例模式实现方式,它可以在保证线程安全的同时,避免了每次调用 getInstance()
方法都需要加锁的性能问题。
具体来说,双重检查锁定的实现方式如下:
class Singleton { public: static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { instance = new Singleton(); } } return instance; } void doSomething() { std::cout << "Singleton doSomething" << std::endl; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton* instance; static std::mutex mutex; }; Singleton* Singleton::instance = nullptr; std::mutex Singleton::mutex; int main() { Singleton* instance1 = Singleton::getInstance(); Singleton* instance2 = Singleton::getInstance(); if (instance1 == instance2) { std::cout << "instance1 and instance2 are the same instance" << std::endl; } else { std::cout << "instance1 and instance2 are different instances" << std::endl; } instance1->doSomething(); instance2->doSomething(); return 0; }
在这个示例中,getInstance()
方法首先判断实例是否已经存在,如果不存在,则获取互斥锁,然后再次判断实例是否已经存在。由于在获取互斥锁之前已经进行了一次判断,因此可以避免多个线程同时获取互斥锁的情况,从而提高了程序的性能。
需要注意的是,在使用双重检查锁定时,需要注意内存可见性问题。由于编译器和处理器的优化,可能会导致变量的值在不同的线程中不一致,从而导致程序出现异常。可以使用 std::atomic
类型来解决这个问题,或者使用 C++11 中引入的跨平台内存屏障(Memory Barrier)来保证内存可见性。
🎂五、饿汉模式怎么保证线程安全
饿汉模式是指在程序启动时就创建单例对象。在这种情况下,由于实例已经被创建,因此不存在多个线程同时创建实例的问题。所以,饿汉模式本身就是线程安全的。
但是,如果在程序启动时需要进行大量的初始化工作,可能会影响程序的启动速度。因此,可以考虑将初始化工作延迟到实例被第一次使用时再进行,这样可以避免不必要的初始化工作,提高程序的启动速度。
如果需要在饿汉模式下延迟初始化工作,可以使用静态成员变量和静态成员函数的方式来实现。静态成员变量只会在第一次使用时被初始化,因此可以实现延迟初始化的效果。示例代码如下:
class Singleton { public: static Singleton* getInstance() { return &getInstanceImpl(); } void doSomething() { std::cout << "Singleton doSomething" << std::endl; } private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static Singleton& getInstanceImpl() { static Singleton instance; return instance; } }; int main() { Singleton* instance1 = Singleton::getInstance(); Singleton* instance2 = Singleton::getInstance(); if (instance1 == instance2) { std::cout << "instance1 and instance2 are the same instance" << std::endl; } else { std::cout << "instance1 and instance2 are different instances" << std::endl; } instance1->doSomething(); instance2->doSomething(); return 0; }
在这个示例中,getInstanceImpl()
方法返回的是一个静态局部变量 instance
的引用。由于静态局部变量只会在第一次使用时被初始化,因此可以实现延迟初始化的效果。在 getInstance()
方法中,直接返回 getInstanceImpl()
方法的返回值即可。
需要注意的是,在使用静态局部变量时,需要考虑线程安全问题。在多线程环境下,可能会出现多个线程同时访问静态局部变量的情况,从而导致程序出现异常。可以使用 C++11 中引入的线程安全的局部静态变量(Thread-safe Local Static)来解决这个问题。
🥩六、注意事项
单例模式是一种常用的设计模式,但是在使用时需要注意以下几点:
- 线程安全性:单例模式在多线程环境下需要保证线程安全性,否则可能会导致多个线程同时创建实例的问题。可以使用懒汉模式、饿汉模式、双重检查锁定等方式来保证线程安全性。
- 内存泄漏:单例模式在程序运行期间只会创建一个实例,如果实例没有被正确释放,可能会导致内存泄漏问题。可以使用智能指针等方式来避免内存泄漏问题。
- 可维护性:单例模式可能会导致代码的可维护性下降,因为单例模式隐藏了对象的创建和销毁过程,使得代码的调试和修改变得困难。因此,在使用单例模式时需要注意代码的可维护性。
- 应用场景:单例模式适用于需要全局唯一实例的场景,例如配置文件、日志系统等。但是,在一些场景下,单例模式可能会导致代码的复杂性增加,因此需要根据实际情况来选择是否使用单例模式。
- 可测试性:单例模式可能会导致代码的可测试性下降,因为单例模式隐藏了对象的创建和销毁过程,使得代码的测试变得困难。因此,在使用单例模式时需要注意代码的可测试性。
🍳参考文献
🧊文章总结
提示:这里对文章进行总结:
本文讲了单例模式的一些注意事项,希望大家学习后有所收获。