1.初识单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
Singleton:负责创建Singleton类自己的唯一实例,并提供一个getInstance的方法,让外部来访问这个类的唯一实例。
1.1 C++实现
//GOF提供的参考
class Singleton{
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* m_instance;
};
//实现
Singleton* Singleton::m_instance = nullptr;
Singleton* Singleton::Instance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
- 使用惰性(lazy)初始化,Instance返回值直到第一次访问时才创建和保存。
- Singleton
定义一个Instance操作,允许客户访问它的唯一实例。Instance是一个类操作(C++中的一个静态成员函数)。
可能负责创建它自己的唯一实例。 - 客户只能通过Singleton的Instance操作访问一个Singleton的实例。
singleton单例的github源码
- 说明1.关于c++ 11 static线程安全
static只保证其本身的变量多线程是安全的,如果是指针的话,并不能保证其指向的内容是多线程安全!这也是3.1 3.2 与 3.3之间的差别! - 说明2.关于多线程安全
假设两个线程同时判定m_instance为nullptr,则两个线程都会创建一个实例,此时就会出现问题。
因此在多线程环境里面,需要加上一个同步锁。 - 说明3.加锁是个耗时操作
因此加上一个判断,只在为nullptr时才加锁。
2.体会单例模式
2.1 场景问题——读取配置文件的内容
现在要读取配置文件的内容,该如何实现呢?
2.2 不用模式的解决方案
存在的问题:
在系统运行期间,系统中会存在很多个AppConfig的实例对象,这会严重浪费系统资源。
把上面的描述进一步抽象一下,问题就出来了:在一个系统运行期间,某个类只需要一个类实例就可以了,那么应该怎么实现呢?
2.3 使用模式的解决方案
3.理解单例模式
3.1 认识单例模式
1.单例模式的功能单例模式的功能是用来保证这个类在运行期间只会被创建一个类实例,并提供一个全局唯一访问这个类实例的访问点。
2.单例模式的范围是一个ClassLoader及其子ClassLoader的范围
3.单例模式的命名一般建议单例模式的方法命名为:getInstance() 。
单例模式的名称:单例、单件、单体等等,翻译的不同,都是指的同一个模式
3.2 懒汉式和饿汉式实现
3.2.1 Java懒汉式
3.2.2 Java饿汉式
3.2.3 C++单线程实现(懒汉式)
//h文件
#include
#include
#ifndef SINGLETON_H
#define SINGLETON_H
class Singleton {
public:
static Singleton* GetInstance();
static void DestroyInstance();
private:
Singleton() {
std::cout << "create a singleton" << std::endl;
}
class SingletonDel {
public:
~SingletonDel() {
if (m_instance != nullptr) {
delete m_instance;
m_instance = nullptr;
}
}
};
static SingletonDel m_singleton_del;
static Singleton *m_instance;
};
#endif
//cc文件
#include
#include
#include "singleton.h"
Singleton* Singleton::m_instance = nullptr;
Singleton* Singleton::GetInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
std::cout << "m_instance: " << m_instance << std::endl;
return m_instance;
}
void Singleton::DestroyInstance() {
if (m_instance != nullptr) {
delete m_instance;
m_instance = nullptr;
}
}
#include
#include "singleton.h"
int main() {
std::thread t1(Singleton::GetInstance);
t1.detach();
std::thread t2(Singleton::GetInstance);
t2.detach();
std::thread t3(Singleton::GetInstance);
t3.detach();
std::thread t4(Singleton::GetInstance);
t4.detach();
std::thread t5(Singleton::GetInstance);
t5.detach();
std::thread t6(Singleton::GetInstance);
t6.detach();
std::thread t7(Singleton::GetInstance);
t7.detach();
return 0;
}
3.2.4 C++加锁实现线程安全(线程安全的懒汉式)
#include
#include
#ifndef THREAD_SAFE_SINGLETON_H
#define THREAD_SAFE_SINGLETON_H
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton* GetInstance();
static void DestroyInstance();
private:
ThreadSafeSingleton() {
std::cout << "create a thread safe singleton! " << std::endl;
}
class SingletonDel{
public:
~SingletonDel() {
if (m_instance != nullptr) {
delete m_instance;
m_instance = nullptr;
}
}
};
static ThreadSafeSingleton m_singleton_del;
static ThreadSafeSingleton *m_instance;
};
#endif
#include "ThreadSafeSingleton.h"
#include
#include
#include
namespace thread_safe_singleton_private {
static std::mutex thread_mutex;
}
ThreadSafeSingleton* ThreadSafeSingleton::m_instance = nullptr;
ThreadSafeSingleton* ThreadSafeSingleton::GetInstance() {
if (m_instance == nullptr) {
thread_safe_singleton_private::thread_mutex.lock();
if (m_instance == nullptr) {
m_instance = new ThreadSafeSingleton();
}
thread_safe_singleton_private::thread_mutex.unlock();
}
std::cout << "m_instance: " << m_instance << std::endl;
return m_instance;
}
void ThreadSafeSingleton::DestroyInstance() {
if (m_instance != nullptr) {
delete m_instance;
m_instance = nullptr;
}
}
- 使用c++11的lock_guard
如下这段代码有个问题:
第三行代码:if (pInstance == nullptr)和第六行代码pInstance = new Widget();没有正确的同步,在某种情况下会出现new返回了地址赋值给pInstance变量而Widget此时还没有构造完全,当另一个线程随后运行到第三行时将不会进入if从而返回了不完全的实例对象给用户使用,造成了严重的错误。在C++11没有出来的时候,只能靠插入两个memory barrier来解决这个错误,但是C++11已经出现了好几年了,其中我认为最重要的是引进了memory model,从此C++11也能识别线程这个概念了!
Widget* Widget::pInstance{ nullptr };
Widget* Widget::Instance() {
if (pInstance == nullptr) { // 1: first check
lock_guard lock{ mutW };
if (pInstance == nullptr) { // 2: second check
pInstance = new Widget();
}
}
return pInstance;
}
- 正确版本
C++11中的atomic类的默认memory_order_seq_cst保证了3、6行代码的正确同步.
atomic Widget::pInstance{ nullptr };
Widget* Widget::Instance() {
if (pInstance == nullptr) {
lock_guard lock{ mutW };
if (pInstance == nullptr) {
pInstance = new Widget();
}
}
return pInstance;
}
- 优化版本
由于上面的atomic需要一些性能上的损失,因此我们可以写一个优化的版本:(p不是atomic?)
atomic Widget::pInstance{ nullptr };
Widget* Widget::Instance() {
Widget* p = pInstance;
if (p == nullptr) {
lock_guard lock{ mutW };
if ((p = pInstance) == nullptr) {
pInstance = p = new Widget();
}
}
return p;
}
- call_once实现单例模式
1 static unique_ptr widget::instance;
2 static std::once_flag widget::create;
3 widget& widget::get_instance() {
4 std::call_once(create, [=]{ instance = make_unique(); });
5 return instance;
6 }
3.2.5 c++11静态变量初始化是线程安全的(饿汉式)
1 widget& widget::get_instance() {
2 static widget instance;
3 return instance;
4 }
#include
#ifndef CPP11_SINGLETON_H
#define CPP11_SINGLETON_H
template
class Cpp11Singleton {
public:
static T* GetInstance() {
static T instance;
std::cout << "this: " << &instance << std::endl;
return &instance;
}
};
class TestCpp11Singleton {
friend class Cpp11Singleton;
private:
TestCpp11Singleton() {
std::cout << "create a cpp11 singleton! " << this << std::endl;
}
};
#endif
3.3 延迟加载的思想
什么是延迟加载呢?
通俗点说,就是一开始不要加载资源或者数据,一直等到马上就要使用这个资源或者数据了,躲不过去了才加载,所以也称Lazy Load,不是懒惰啊,是 “延迟加载”,这在实际开发中是一种很常见的思想,尽可能的节约资源。
3.4 缓存的思想
单例模式的懒汉式实现还体现了缓存的思想,缓存也是实际开发中非常常见的功能。
简单讲就是,如果某些资源或者数据会被频繁的使用,可以把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,那么就直接使用,如果没有那么就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取了。从而节省大量的时间,当然,缓存是一种典型的空间换时间的方案。
3.5 Java中缓存的基本实现
3.6 利用缓存来实现单例模式
3.7 单例模式的优缺点
-
时间和空间:懒汉式是典型的时间换空间,饿汉式是典型的空间换时间 * * * 线程安全:
1)不加同步的懒汉式是线程不安全的
2)饿汉式是线程安全的,因为虚拟机保证了只会装载一次
3)如何实现懒汉式的线程安全呢?加上synchronized即可
4)双重检查加锁
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被 volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
注意:在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此本机制只能用在Java5及以上的版本。
优点:
- 对唯一实例的受控访问。
- 缩小名空间。
单例模式是对全局变量的一种改进,避免了全局变量污染全局命名空间。 - 允许对操作和表示的精化。
Singleton类可以有子类。(此时构造函数应该定义为protected) - 允许可变数目的实例
- 比类操作更灵活
3.8 在Java中一种更好的单例实现方式
Lazy initialization holder class模式,这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙的同时实现了延迟加载和线程安全。
3.9 单例和枚举
按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现 Singleton的最佳方法。
为了理解这个观点,先来了解一点相关的枚举知识,这里只是强化和总结一下枚举的一些重要观点,更多基本的枚举的使用,请参看Java编程入门资料:
1)Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法
2)Java枚举类型的基本思想:通过公有的静态final域为每个枚举常量导出实例的类
3)从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举
用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
4.思考单例模式
4.1 单例模式的本质
单例模式的本质是:控制实例数目
4.2 何时选用
当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题
参考
- 1)研磨设计模式——跟着cc学设计系列视频教程